bianjunhui 20 часов назад
Родитель
Сommit
7f7daed44f

+ 26 - 0
packages/Longyi/Email/composer.json

@@ -0,0 +1,26 @@
+{
+    "name": "longyi/email",
+    "description": "Email logging and template management for Bagisto",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Longyi",
+            "email": "dev@longyi.com"
+        }
+    ],
+    "minimum-stability": "dev",
+    "require": {},
+    "autoload": {
+        "psr-4": {
+            "Longyi\\Email\\": "src/"
+        }
+    },
+    "extra": {
+        "laravel": {
+            "providers": [
+                "Longyi\\Email\\Providers\\EmailServiceProvider"
+            ]
+        }
+    }
+}

+ 68 - 0
packages/Longyi/Email/src/Console/Commands/TestEmailLogging.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace Longyi\Email\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Mail;
+use Webkul\Customer\Models\Customer;
+
+class TestEmailLogging extends Command
+{
+    protected $signature = 'email:test-logging {email?}';
+    protected $description = 'Test email logging functionality';
+
+    public function handle()
+    {
+        $email = $this->argument('email') ?? 'test@example.com';
+
+        $this->info("Testing email logging...");
+        $this->info("Target email: {$email}");
+
+        // 获取或创建测试客户
+        $customer = Customer::firstOrCreate(
+            ['email' => $email],
+            [
+                'first_name' => 'Test',
+                'last_name' => 'User',
+                'password' => bcrypt('password'),
+                'is_verified' => 1,
+                'status' => 1,
+                'channel_id' => 1,
+                'customer_group_id' => 1,
+            ]
+        );
+
+        $this->info("Customer ID: {$customer->id}");
+
+        try {
+            // 发送测试邮件
+            Mail::raw('This is a test email at ' . now(), function ($message) use ($customer) {
+                $message->to($customer->email)
+                    ->subject('Test Email ' . now());
+            });
+
+            $this->info("✓ Email sent successfully!");
+
+            // 等待一下
+            sleep(2);
+
+            // 检查日志
+            $logs = \Longyi\Email\Models\EmailLog::where('recipient_email', $email)
+                ->orderBy('created_at', 'desc')
+                ->get();
+
+            if ($logs->isEmpty()) {
+                $this->warn("No email logs found!");
+                $this->info("Check storage/logs/laravel.log for errors");
+            } else {
+                $this->info("Found " . $logs->count() . " email log(s):");
+                foreach ($logs as $log) {
+                    $this->line("  - {$log->recipient_email}: {$log->status} - {$log->subject}");
+                }
+            }
+
+        } catch (\Exception $e) {
+            $this->error("Failed: " . $e->getMessage());
+        }
+    }
+}

+ 12 - 19
packages/Longyi/Email/src/Database/Migrations/2026_04_20_000001_create_email_logs_table.php

@@ -6,35 +6,28 @@ use Illuminate\Support\Facades\Schema;
 
 return new class extends Migration
 {
-    /**
-     * Run the migrations.
-     */
-    public function up(): void
+    public function up()
     {
         Schema::create('email_logs', function (Blueprint $table) {
             $table->id();
-            $table->string('recipient_email')->comment('收件人邮箱');
-            $table->string('recipient_name')->nullable()->comment('收件人姓名');
-            $table->string('subject')->comment('邮件主题');
-            $table->longText('content')->nullable()->comment('邮件内容');
-            $table->string('template')->nullable()->comment('使用的模板');
-            $table->string('status')->default('pending')->comment('状态: pending, sent, failed');
-            $table->text('error_message')->nullable()->comment('错误信息');
-            $table->json('metadata')->nullable()->comment('元数据');
-            $table->timestamp('sent_at')->nullable()->comment('发送时间');
+            $table->string('recipient_email');
+            $table->string('recipient_name')->nullable();
+            $table->string('subject');
+            $table->longText('content')->nullable();
+            $table->string('template')->nullable();
+            $table->enum('status', ['pending', 'sent', 'failed'])->default('pending');
+            $table->string('type')->nullable(); // registration, order, reset_password, etc.
+            $table->json('metadata')->nullable();
+            $table->timestamp('sent_at')->nullable();
             $table->timestamps();
 
-            // 索引优化查询性能
             $table->index(['recipient_email', 'status']);
+            $table->index(['type', 'created_at']);
             $table->index('created_at');
-            $table->index('status');
         });
     }
 
-    /**
-     * Reverse the migrations.
-     */
-    public function down(): void
+    public function down()
     {
         Schema::dropIfExists('email_logs');
     }

+ 2 - 40
packages/Longyi/Email/src/Models/EmailLog.php

@@ -3,7 +3,6 @@
 namespace Longyi\Email\Models;
 
 use Illuminate\Database\Eloquent\Model;
-use Illuminate\Database\Eloquent\Casts\AsArrayObject;
 
 class EmailLog extends Model
 {
@@ -16,50 +15,13 @@ class EmailLog extends Model
         'content',
         'template',
         'status',
-        'error_message',
+        'type',
         'metadata',
         'sent_at',
     ];
 
     protected $casts = [
-        'metadata' => AsArrayObject::class,
+        'metadata' => 'array',
         'sent_at' => 'datetime',
     ];
-
-    /**
-     * Scope: 成功的邮件
-     */
-    public function scopeSuccessful($query)
-    {
-        return $query->where('status', 'sent');
-    }
-
-    /**
-     * Scope: 失败的邮件
-     */
-    public function scopeFailed($query)
-    {
-        return $query->where('status', 'failed');
-    }
-
-    /**
-     * Scope: 待发送的邮件
-     */
-    public function scopePending($query)
-    {
-        return $query->where('status', 'pending');
-    }
-
-    /**
-     * 获取状态文本
-     */
-    public function getStatusTextAttribute(): string
-    {
-        return match($this->status) {
-            'sent' => '已发送',
-            'failed' => '发送失败',
-            'pending' => '待发送',
-            default => '未知',
-        };
-    }
 }

+ 1 - 0
packages/Longyi/Email/src/Providers/EmailServiceProvider.php

@@ -30,6 +30,7 @@ class EmailServiceProvider extends ServiceProvider
         if ($this->app->runningInConsole()) {
             $this->commands([
                 \Longyi\Email\Console\Commands\InitializeSettings::class,
+                \Longyi\Email\Console\Commands\TestEmailLogging::class,
             ]);
         }
     }

+ 753 - 61
packages/Longyi/Email/src/Providers/EventServiceProvider.php

@@ -2,109 +2,801 @@
 
 namespace Longyi\Email\Providers;
 
-use Illuminate\Support\Facades\Event;
 use Illuminate\Support\ServiceProvider;
 use Longyi\Email\Models\EmailLog;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Mail;
+use Illuminate\Mail\Events\MessageSending;
+use Illuminate\Mail\Events\MessageSent;
+use Illuminate\Support\Facades\Event;
 
 class EventServiceProvider extends ServiceProvider
 {
     public function boot(): void
     {
-        // 监听邮件发送前事件
+        Log::info('Email EventServiceProvider booting...');
+
+        // 方法1: 监听 Laravel 邮件事件(同步和异步都有效)
+        $this->registerMailEvents();
+
+        // 方法2: 监听队列事件(作为备用)
+        $this->registerQueueEvents();
+
+        // 方法3: Hook Mail facade(最可靠)
+        $this->hookMailFacade();
+
+        Log::info('Email EventServiceProvider booted successfully');
+    }
+
+    /**
+     * 注册邮件事件监听器
+     */
+    protected function registerMailEvents(): void
+    {
+        // Laravel 9+ 使用这些事件
+        Event::listen(MessageSending::class, function (MessageSending $event) {
+            Log::info('MessageSending event triggered', [
+                'message_id' => $event->message->getMessageId(),
+            ]);
+
+            $this->logEmailFromMessage($event->message, 'pending');
+        });
+
+        Event::listen(MessageSent::class, function (MessageSent $event) {
+            Log::info('MessageSent event triggered');
+
+            $this->updateEmailStatus($event->message, 'sent');
+        });
+
+        // 兼容旧版本的事件名称
         Event::listen('illuminate.mail.sending', function ($event) {
+            Log::info('illuminate.mail.sending event triggered');
+
+            if (isset($event->message)) {
+                $this->logEmailFromMessage($event->message, 'pending');
+            }
+        });
+
+        Event::listen('illuminate.mail.sent', function ($event) {
+            Log::info('illuminate.mail.sent event triggered');
+
+            if (isset($event->message)) {
+                $this->updateEmailStatus($event->message, 'sent');
+            }
+        });
+    }
+
+    /**
+     * 注册队列事件监听器
+     */
+    protected function registerQueueEvents(): void
+    {
+        // 监听邮件任务被推入队列时
+        Event::listen('Illuminate\Queue\Events\JobQueued', function ($event) {
             try {
-                $message = $event->message;
+                // JobQueued 事件有不同的属性结构
+                $payload = null;
+
+                if (is_object($event)) {
+                    // Laravel 10+ 的结构
+                    if (isset($event->payload)) {
+                        $payload = $event->payload;
+                    }
+                    // 或者通过 job 获取
+                    elseif (isset($event->job)) {
+                        $payload = $event->job->payload();
+                    }
+                }
+
+                // 如果 payload 是字符串,解析为数组
+                if (is_string($payload)) {
+                    $payload = json_decode($payload, true);
+                }
+
+                // 如果还是无法获取 payload,直接返回
+                if (!is_array($payload) || !isset($payload['data']['command'])) {
+                    return;
+                }
+
+                $command = @unserialize($payload['data']['command']);
+
+                if (!$command) {
+                    return;
+                }
+
+                // 检查是否是 SendQueuedMailable
+                if (!$command instanceof \Illuminate\Mail\SendQueuedMailable) {
+                    return;
+                }
 
-                // 获取收件人
-                $to = $message->getTo();
-                if (empty($to)) {
+                $mailable = $command->mailable;
+
+                if (!$mailable instanceof \Illuminate\Mail\Mailable) {
                     return;
                 }
 
-                foreach ($to as $email => $name) {
-                    EmailLog::create([
+                Log::info('Mail job queued', [
+                    'mailable_class' => get_class($mailable),
+                ]);
+
+                // 关键修复:从 SendQueuedMailable 命令中提取收件人
+                $recipients = $this->extractRecipientsFromCommand($command, $mailable);
+
+                if (empty($recipients)) {
+                    Log::warning('No recipients found in command or mailable');
+                    return;
+                }
+
+                $subject = $this->extractSubjectFromMailable($mailable);
+
+                // 记录日志
+                foreach ($recipients as $recipient) {
+                    $email = is_array($recipient) ? ($recipient['address'] ?? $recipient['email'] ?? null) : $recipient;
+                    $name = is_array($recipient) ? ($recipient['name'] ?? null) : null;
+
+                    if (!$email) {
+                        continue;
+                    }
+
+                    // 检查是否已存在
+                    $exists = EmailLog::where('recipient_email', $email)
+                        ->where('subject', $subject)
+                        ->where('created_at', '>', now()->subMinutes(5))
+                        ->exists();
+
+                    if ($exists) {
+                        continue;
+                    }
+
+                    $log = EmailLog::create([
                         'recipient_email' => $email,
-                        'recipient_name' => is_string($name) ? $name : null,
-                        'subject' => $message->getSubject() ?? '',
-                        'content' => $message->getBody()?->toString() ?? '',
-                        'template' => $this->extractTemplateName($message),
-                        'status' => 'pending',
-                        'metadata' => [
-                            'from' => $this->formatAddresses($message->getFrom()),
-                            'cc' => $this->formatAddresses($message->getCc()),
-                            'bcc' => $this->formatAddresses($message->getBcc()),
-                            'reply_to' => $this->formatAddresses($message->getReplyTo()),
-                        ],
+                        'recipient_name' => $name,
+                        'subject' => $subject,
+                        'content' => $this->extractContentFromMailable($mailable),
+                        'template' => $this->detectTemplateFromMailable($mailable),
+                        'status' => 'queued',
+                        'metadata' => json_encode([
+                            'job_id' => $payload['uuid'] ?? null,
+                            'mailable_class' => get_class($mailable),
+                            'queue' => $payload['queue'] ?? null,
+                        ]),
+                    ]);
+
+                    Log::info('Email log created from queued job', [
+                        'log_id' => $log->id,
+                        'email' => $email,
+                        'subject' => $subject,
                     ]);
                 }
+
             } catch (\Exception $e) {
-                \Log::error('Failed to log email sending: ' . $e->getMessage());
+                Log::error('Failed to process queued mail job: ' . $e->getMessage(), [
+                    'trace' => $e->getTraceAsString(),
+                ]);
             }
         });
 
-        // 监听邮件发送成功事件
-        Event::listen('illuminate.mail.sent', function ($event) {
-            try {
-                $message = $event->message;
-                $to = $message->getTo();
+        // 监听队列任务处理前
+        \Illuminate\Support\Facades\Queue::before(function (\Illuminate\Queue\Events\JobProcessing $event) {
+            $this->handleQueueJob($event->job, 'processing');
+        });
 
-                if (empty($to)) {
-                    return;
+        // 监听队列任务处理后
+        \Illuminate\Support\Facades\Queue::after(function (\Illuminate\Queue\Events\JobProcessed $event) {
+            $this->handleQueueJobAfter($event->job);
+        });
+
+        // 监听队列任务失败
+        \Illuminate\Support\Facades\Queue::failing(function (\Illuminate\Queue\Events\JobFailed $event) {
+            $this->handleQueueJobFailed($event->job, $event->exception);
+        });
+    }
+
+    /**
+     * 从 SendQueuedMailable 命令中提取收件人
+     */
+    protected function extractRecipientsFromCommand($command, $mailable): array
+    {
+        try {
+            Log::info('Extracting recipients from SendQueuedMailable command');
+
+            // 方法1: 从命令对象的 to 属性获取(Laravel 会在构建时填充)
+            if (property_exists($command, 'to') && !empty($command->to)) {
+                Log::info('Found recipients in command->to', ['to' => $command->to]);
+                return $command->to;
+            }
+
+            // 方法2: 尝试反射获取命令的 to 属性
+            $reflection = new \ReflectionClass($command);
+            if ($reflection->hasProperty('to')) {
+                $property = $reflection->getProperty('to');
+                $property->setAccessible(true);
+                $to = $property->getValue($command);
+
+                if (!empty($to)) {
+                    Log::info('Extracted recipients from command via reflection', ['to' => $to]);
+                    return $to;
                 }
+            }
 
-                foreach ($to as $email => $name) {
-                    // 更新最近的一条 pending 日志为 sent
-                    EmailLog::where('recipient_email', $email)
-                        ->where('status', 'pending')
-                        ->orderBy('created_at', 'desc')
-                        ->limit(1)
-                        ->update([
-                            'status' => 'sent',
-                            'sent_at' => now(),
-                        ]);
+            // 方法3: 从 Mailable 的 customer 属性推断(针对 RegistrationNotification)
+            if (property_exists($mailable, 'customer')) {
+                $reflection = new \ReflectionClass($mailable);
+                $property = $reflection->getProperty('customer');
+                $property->setAccessible(true);
+                $customer = $property->getValue($mailable);
+
+                if ($customer && isset($customer->email)) {
+                    $recipients = [[
+                        'address' => $customer->email,
+                        'name' => $customer->name ?? $customer->first_name ?? null,
+                    ]];
+                    Log::info('Extracted recipient from mailable customer', ['recipients' => $recipients]);
+                    return $recipients;
                 }
-            } catch (\Exception $e) {
-                \Log::error('Failed to update email log: ' . $e->getMessage());
             }
-        });
 
-        // 监听邮件发送失败(通过队列失败事件)
-        Event::listen('Illuminate\Queue\Events\JobFailed', function ($event) {
-            try {
-                $payload = $event->job->payload();
+            // 方法4: 调用 envelope 方法
+            if (method_exists($mailable, 'envelope')) {
+                try {
+                    $envelope = $mailable->envelope();
+                    if ($envelope && method_exists($envelope, 'to')) {
+                        $to = $envelope->to();
+                        if (!empty($to)) {
+                            Log::info('Extracted recipients from envelope', ['to' => $to]);
+                            return $to;
+                        }
+                    }
+                } catch (\Exception $e) {
+                    Log::warning('Failed to get envelope: ' . $e->getMessage());
+                }
+            }
+
+            Log::warning('Could not extract recipients from command or mailable', [
+                'command_class' => get_class($command),
+                'mailable_class' => get_class($mailable),
+            ]);
 
-                // 检查是否是邮件任务
-                if (isset($payload['data']['command']) &&
-                    str_contains($payload['data']['command'], 'SendQueuedMailable')) {
+            return [];
+        } catch (\Exception $e) {
+            Log::error('Extract recipients from command error: ' . $e->getMessage(), [
+                'trace' => $e->getTraceAsString(),
+            ]);
+            return [];
+        }
+    }
+
+    /**
+     * Hook Mail facade 以捕获所有邮件发送
+     */
+    protected function hookMailFacade(): void
+    {
+        // 这个方法通过扩展 Mailer 来实现
+        // 但由于复杂性,我们主要依赖事件监听
+    }
+
+    /**
+     * 从 Swift_Message 对象记录邮件
+     */
+    protected function logEmailFromMessage($message, string $status): void
+    {
+        try {
+            $to = $message->getTo();
+
+            if (empty($to)) {
+                Log::warning('No recipients in message');
+                return;
+            }
+
+            foreach ($to as $email => $name) {
+                // 检查是否已存在(避免重复)
+                $exists = EmailLog::where('recipient_email', $email)
+                    ->where('subject', $message->getSubject() ?? '')
+                    ->where('created_at', '>', now()->subMinutes(5))
+                    ->exists();
+
+                if ($exists) {
+                    Log::info('Email log already exists, skipping', ['email' => $email]);
+                    continue;
+                }
+
+                $log = EmailLog::create([
+                    'recipient_email' => $email,
+                    'recipient_name' => is_string($name) ? $name : null,
+                    'subject' => $message->getSubject() ?? '',
+                    'content' => $this->extractContentFromMessage($message),
+                    'template' => $this->detectTemplateFromMessage($message),
+                    'status' => $status,
+                    'metadata' => json_encode([
+                        'from' => $this->formatAddresses($message->getFrom()),
+                        'cc' => $this->formatAddresses($message->getCc()),
+                        'bcc' => $this->formatAddresses($message->getBcc()),
+                        'reply_to' => $this->formatAddresses($message->getReplyTo()),
+                        'message_id' => $message->getMessageId(),
+                    ]),
+                ]);
+
+                Log::info('Email log created from message', [
+                    'log_id' => $log->id,
+                    'email' => $email,
+                    'status' => $status,
+                ]);
+            }
+        } catch (\Exception $e) {
+            Log::error('Failed to log email from message: ' . $e->getMessage(), [
+                'trace' => $e->getTraceAsString(),
+            ]);
+        }
+    }
 
-                    // 这里可以尝试解析并标记失败的邮件
-                    \Log::info('Email job failed', [
-                        'exception' => $event->exception->getMessage(),
+    /**
+     * 从 Mailable 对象记录邮件
+     */
+    protected function logEmailFromMailable($mailable, string $status, ?string $jobId = null): void
+    {
+        try {
+            // 提取收件人
+            $to = $this->extractRecipientsFromMailable($mailable);
+
+            if (empty($to)) {
+                Log::warning('No recipients in mailable');
+                return;
+            }
+
+            $subject = $this->extractSubjectFromMailable($mailable);
+
+            foreach ($to as $recipient) {
+                $email = is_array($recipient) ? ($recipient['address'] ?? $recipient['email'] ?? null) : $recipient;
+                $name = is_array($recipient) ? ($recipient['name'] ?? null) : null;
+
+                if (!$email) {
+                    continue;
+                }
+
+                // 检查是否已存在
+                $exists = EmailLog::where('recipient_email', $email)
+                    ->where('subject', $subject)
+                    ->where('created_at', '>', now()->subMinutes(5))
+                    ->exists();
+
+                if ($exists) {
+                    continue;
+                }
+
+                $log = EmailLog::create([
+                    'recipient_email' => $email,
+                    'recipient_name' => $name,
+                    'subject' => $subject,
+                    'content' => $this->extractContentFromMailable($mailable),
+                    'template' => $this->detectTemplateFromMailable($mailable),
+                    'status' => $status,
+                    'metadata' => json_encode([
+                        'job_id' => $jobId,
+                        'mailable_class' => get_class($mailable),
+                        'cc' => $this->extractCcFromMailable($mailable),
+                        'bcc' => $this->extractBccFromMailable($mailable),
+                    ]),
+                ]);
+
+                Log::info('Email log created from mailable', [
+                    'log_id' => $log->id,
+                    'email' => $email,
+                    'status' => $status,
+                ]);
+            }
+        } catch (\Exception $e) {
+            Log::error('Failed to log email from mailable: ' . $e->getMessage(), [
+                'trace' => $e->getTraceAsString(),
+            ]);
+        }
+    }
+
+    /**
+     * 更新邮件状态
+     */
+    protected function updateEmailStatus($message, string $status): void
+    {
+        try {
+            $to = $message->getTo();
+
+            if (empty($to)) {
+                return;
+            }
+
+            foreach ($to as $email => $name) {
+                $updated = EmailLog::where('recipient_email', $email)
+                    ->whereIn('status', ['pending', 'queued', 'processing'])
+                    ->orderBy('created_at', 'desc')
+                    ->limit(1)
+                    ->update([
+                        'status' => $status,
+                        'sent_at' => now(),
+                    ]);
+
+                if ($updated) {
+                    Log::info('Email status updated', [
+                        'email' => $email,
+                        'status' => $status,
                     ]);
                 }
-            } catch (\Exception $e) {
-                \Log::error('Failed to log email failure: ' . $e->getMessage());
             }
-        });
+        } catch (\Exception $e) {
+            Log::error('Failed to update email status: ' . $e->getMessage());
+        }
     }
 
     /**
-     * 提取模板名称
+     * 处理队列任务
      */
-    protected function extractTemplateName($message): ?string
+    protected function handleQueueJob($job, string $status): void
     {
-        // 尝试从消息头或内容中提取模板信息
-        $headers = $message->getHeaders();
+        try {
+            $payload = $job->payload();
+
+            if (!isset($payload['data']['command'])) {
+                return;
+            }
+
+            $command = @unserialize($payload['data']['command']);
+
+            if (!$command || !property_exists($command, 'mailable')) {
+                return;
+            }
 
-        if ($headers->has('X-Template-Name')) {
-            return $headers->get('X-Template-Name')->getFieldBody();
+            $mailable = $command->mailable;
+
+            if (!$mailable instanceof \Illuminate\Mail\Mailable) {
+                return;
+            }
+
+            $this->logEmailFromMailable($mailable, $status, $job->getJobId());
+
+        } catch (\Exception $e) {
+            Log::error('Handle queue job error: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 处理队列任务完成后
+     */
+    protected function handleQueueJobAfter($job): void
+    {
+        try {
+            $payload = $job->payload();
+
+            if (!isset($payload['data']['command'])) {
+                return;
+            }
+
+            $command = @unserialize($payload['data']['command']);
+
+            if (!$command || !property_exists($command, 'mailable')) {
+                return;
+            }
+
+            $mailable = $command->mailable;
+
+            if (!$mailable instanceof \Illuminate\Mail\Mailable) {
+                return;
+            }
+
+            $to = $this->extractRecipientsFromMailable($mailable);
+            $subject = $this->extractSubjectFromMailable($mailable);
+
+            foreach ($to as $recipient) {
+                $email = is_array($recipient) ? ($recipient['address'] ?? $recipient['email'] ?? null) : $recipient;
+
+                if (!$email) {
+                    continue;
+                }
+
+                EmailLog::where('recipient_email', $email)
+                    ->where('subject', $subject)
+                    ->whereIn('status', ['queued', 'processing'])
+                    ->orderBy('created_at', 'desc')
+                    ->limit(1)
+                    ->update([
+                        'status' => 'sent',
+                        'sent_at' => now(),
+                    ]);
+            }
+
+        } catch (\Exception $e) {
+            Log::error('Handle queue job after error: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 处理队列任务失败
+     */
+    protected function handleQueueJobFailed($job, $exception): void
+    {
+        try {
+            $payload = $job->payload();
+
+            if (!isset($payload['data']['command'])) {
+                return;
+            }
+
+            $command = @unserialize($payload['data']['command']);
+
+            if (!$command || !property_exists($command, 'mailable')) {
+                return;
+            }
+
+            $mailable = $command->mailable;
+
+            if (!$mailable instanceof \Illuminate\Mail\Mailable) {
+                return;
+            }
+
+            $to = $this->extractRecipientsFromMailable($mailable);
+            $subject = $this->extractSubjectFromMailable($mailable);
+
+            foreach ($to as $recipient) {
+                $email = is_array($recipient) ? ($recipient['address'] ?? $recipient['email'] ?? null) : $recipient;
+
+                if (!$email) {
+                    continue;
+                }
+
+                EmailLog::where('recipient_email', $email)
+                    ->where('subject', $subject)
+                    ->whereIn('status', ['queued', 'processing'])
+                    ->orderBy('created_at', 'desc')
+                    ->limit(1)
+                    ->update([
+                        'status' => 'failed',
+                        'error_message' => $exception->getMessage(),
+                    ]);
+            }
+
+        } catch (\Exception $e) {
+            Log::error('Handle queue job failed error: ' . $e->getMessage());
+        }
+    }
+
+
+
+    /**
+     * 从 Mailable 提取收件人
+     */
+    protected function extractRecipientsFromMailable($mailable): array
+    {
+        try {
+            Log::info('Extracting recipients from mailable', [
+                'class' => get_class($mailable),
+            ]);
+
+            // 方法1: 尝试直接访问 to 属性
+            if (property_exists($mailable, 'to')) {
+                $to = $mailable->to;
+                Log::info('Found to property', ['to' => $to]);
+                return $to ?? [];
+            }
+
+            // 方法2: 尝试反射获取 to 属性
+            $reflection = new \ReflectionClass($mailable);
+
+            if ($reflection->hasProperty('to')) {
+                $property = $reflection->getProperty('to');
+                $property->setAccessible(true);
+                $to = $property->getValue($mailable);
+                Log::info('Extracted to via reflection', ['to' => $to]);
+                return $to ?? [];
+            }
+
+            // 方法3: 检查构造函数参数(Customer 对象)
+            if ($reflection->hasProperty('customer')) {
+                $property = $reflection->getProperty('customer');
+                $property->setAccessible(true);
+                $customer = $property->getValue($mailable);
+
+                if ($customer && isset($customer->email)) {
+                    $recipients = [[
+                        'address' => $customer->email,
+                        'name' => $customer->name ?? $customer->first_name ?? null,
+                    ]];
+                    Log::info('Extracted recipient from customer property', ['recipients' => $recipients]);
+                    return $recipients;
+                }
+            }
+
+            // 方法4: 尝试调用 envelope 方法获取收件人
+            if (method_exists($mailable, 'envelope')) {
+                try {
+                    $envelope = $mailable->envelope();
+                    if ($envelope && method_exists($envelope, 'to')) {
+                        $to = $envelope->to();
+                        if (!empty($to)) {
+                            Log::info('Extracted to from envelope', ['to' => $to]);
+                            return $to;
+                        }
+                    }
+                } catch (\Exception $e) {
+                    Log::warning('Failed to get envelope to: ' . $e->getMessage());
+                }
+            }
+
+            Log::warning('Could not extract recipients from mailable', [
+                'class' => get_class($mailable),
+                'properties' => array_keys(get_object_vars($mailable)),
+            ]);
+
+            return [];
+        } catch (\Exception $e) {
+            Log::error('Extract recipients error: ' . $e->getMessage(), [
+                'trace' => $e->getTraceAsString(),
+            ]);
+            return [];
+        }
+    }
+
+    /**
+     * 从 Mailable 提取主题
+     */
+    protected function extractSubjectFromMailable($mailable): string
+    {
+        try {
+            Log::info('Extracting subject from mailable', [
+                'class' => get_class($mailable),
+            ]);
+
+            // 方法1: 尝试直接访问 subject 属性
+            if (property_exists($mailable, 'subject')) {
+                $subject = $mailable->subject;
+                Log::info('Found subject property', ['subject' => $subject]);
+                return $subject ?? '';
+            }
+
+            // 方法2: 尝试反射获取 subject 属性
+            $reflection = new \ReflectionClass($mailable);
+
+            if ($reflection->hasProperty('subject')) {
+                $property = $reflection->getProperty('subject');
+                $property->setAccessible(true);
+                $subject = $property->getValue($mailable);
+                Log::info('Extracted subject via reflection', ['subject' => $subject]);
+                return $subject ?? '';
+            }
+
+            // 方法3: 尝试调用 envelope 方法获取主题
+            if (method_exists($mailable, 'envelope')) {
+                try {
+                    $envelope = $mailable->envelope();
+                    if ($envelope && method_exists($envelope, 'subject')) {
+                        $subject = $envelope->subject();
+                        if ($subject) {
+                            Log::info('Extracted subject from envelope', ['subject' => $subject]);
+                            return $subject;
+                        }
+                    }
+                } catch (\Exception $e) {
+                    Log::warning('Failed to get envelope subject: ' . $e->getMessage());
+                }
+            }
+
+            // 方法4: 从类名推断
+            $className = class_basename($mailable);
+            $defaultSubject = str_replace(['Notification', 'Mail'], '', $className);
+            Log::info('Using default subject from class name', ['subject' => $defaultSubject]);
+
+            return $defaultSubject;
+        } catch (\Exception $e) {
+            Log::error('Extract subject error: ' . $e->getMessage());
+            return 'Unknown Subject';
+        }
+    }
+
+
+    /**
+     * 从 Mailable 提取密送
+     */
+    protected function extractBccFromMailable($mailable): array
+    {
+        try {
+            return property_exists($mailable, 'bcc') ? ($mailable->bcc ?? []) : [];
+        } catch (\Exception $e) {
+            return [];
+        }
+    }
+
+    /**
+     * 从 Message 提取内容
+     */
+    protected function extractContentFromMessage($message): string
+    {
+        try {
+            $body = $message->getBody();
+            return $body ? ($body->toString() ?? '') : '';
+        } catch (\Exception $e) {
+            return '';
+        }
+    }
+
+    /**
+     * 从 Mailable 提取内容
+     */
+    protected function extractContentFromMailable($mailable): string
+    {
+        try {
+            if (method_exists($mailable, 'render')) {
+                $content = $mailable->render();
+                return substr($content, 0, 65535);
+            }
+            return '';
+        } catch (\Exception $e) {
+            return '';
+        }
+    }
+
+    /**
+     * 从 Message 检测模板
+     */
+    protected function detectTemplateFromMessage($message): ?string
+    {
+        try {
+            $headers = $message->getHeaders();
+
+            if ($headers && $headers->has('X-Template-Name')) {
+                return $headers->get('X-Template-Name')->getFieldBody();
+            }
+
+            $subject = $message->getSubject();
+            return $this->detectTemplateBySubject($subject);
+        } catch (\Exception $e) {
+            return null;
+        }
+    }
+
+    /**
+     * 从 Mailable 检测模板
+     */
+    protected function detectTemplateFromMailable($mailable): ?string
+    {
+        $className = get_class($mailable);
+        $subject = $this->extractSubjectFromMailable($mailable);
+
+        return $this->detectTemplateBySubject($subject, $className);
+    }
+
+    /**
+     * 根据主题检测模板类型
+     */
+    protected function detectTemplateBySubject(?string $subject, ?string $className = ''): ?string
+    {
+        $text = strtolower(($subject ?? '') . ' ' . ($className ?? ''));
+
+        if (str_contains($text, 'registration')) {
+            return 'customer_registration';
+        }
+        if (str_contains($text, 'verification')) {
+            return 'email_verification';
+        }
+        if (str_contains($text, 'password')) {
+            return 'password_reset';
+        }
+        if (str_contains($text, 'order') && str_contains($text, 'created')) {
+            return 'order_created';
+        }
+        if (str_contains($text, 'invoice')) {
+            return 'order_invoiced';
+        }
+        if (str_contains($text, 'shipped')) {
+            return 'order_shipped';
+        }
+        if (str_contains($text, 'refund')) {
+            return 'order_refunded';
+        }
+        if (str_contains($text, 'cancel')) {
+            return 'order_canceled';
         }
 
-        return null;
+        return 'general';
     }
 
     /**
-     * 格式化地址数组
+     * 格式化地址
      */
     protected function formatAddresses($addresses): array
     {