Browse Source

Merge branch 'dev-rewardPoints' into dev

bianjunhui 4 hours ago
parent
commit
8a0eb4575c
27 changed files with 2741 additions and 189 deletions
  1. 1 0
      bootstrap/providers.php
  2. 1 0
      composer.json
  3. 29 0
      packages/Longyi/DynamicMenu/src/Database/Migrations/add_route_parameters_to_menu_items.php
  4. 148 43
      packages/Longyi/DynamicMenu/src/Http/Controllers/MenuController.php
  5. 7 6
      packages/Longyi/DynamicMenu/src/Models/MenuItem.php
  6. 38 0
      packages/Longyi/DynamicMenu/src/Providers/DynamicMenuServiceProvider.php
  7. 227 79
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/create.blade.php
  8. 275 37
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/edit.blade.php
  9. 16 12
      packages/Longyi/DynamicMenu/src/Routes/admin-routes.php
  10. 34 0
      packages/Longyi/Email/README.md
  11. 26 0
      packages/Longyi/Email/composer.json
  12. 134 0
      packages/Longyi/Email/src/Console/Commands/InitializeSettings.php
  13. 68 0
      packages/Longyi/Email/src/Console/Commands/TestEmailLogging.php
  14. 34 0
      packages/Longyi/Email/src/Database/Migrations/2026_04_20_000001_create_email_logs_table.php
  15. 123 0
      packages/Longyi/Email/src/Http/Controllers/Admin/LogController.php
  16. 82 0
      packages/Longyi/Email/src/Models/EmailLog.php
  17. 45 0
      packages/Longyi/Email/src/Providers/EmailServiceProvider.php
  18. 934 0
      packages/Longyi/Email/src/Providers/EventServiceProvider.php
  19. 49 0
      packages/Longyi/Email/src/Resources/lang/zh_CN/app.php
  20. 225 0
      packages/Longyi/Email/src/Resources/views/admin/logs/index.blade.php
  21. 85 0
      packages/Longyi/Email/src/Resources/views/admin/logs/show.blade.php
  22. 33 0
      packages/Longyi/Email/src/Routes/admin-routes.php
  23. 0 1
      packages/Longyi/RewardPoints/src/Http/Controllers/RewardPointsController.php
  24. 7 2
      packages/Longyi/RewardPoints/src/Listeners/OrderEvents.php
  25. 18 0
      packages/Longyi/RewardPoints/src/Services/LevelCalculationTrait.php
  26. 88 8
      packages/Webkul/Core/src/Menu.php
  27. 14 1
      packages/Webkul/Core/src/Menu/MenuItem.php

+ 1 - 0
bootstrap/providers.php

@@ -12,6 +12,7 @@ return [
     Webkul\Admin\Providers\AdminServiceProvider::class,
     Longyi\Core\Providers\LongyiCoreServiceProvider::class,
     Longyi\DynamicMenu\Providers\DynamicMenuServiceProvider::class,
+    Longyi\Email\Providers\EmailServiceProvider::class,
     Longyi\RewardPoints\Providers\RewardPointsServiceProvider::class,
     Longyi\Member\Providers\MemberServiceProvider::class,
     Longyi\Gift\Providers\GiftServiceProvider::class,

+ 1 - 0
composer.json

@@ -69,6 +69,7 @@
             "Database\\Seeders\\": "database/seeders/",
             "Longyi\\Core\\": "packages/Longyi/Core/src/",
             "Longyi\\DynamicMenu\\": "packages/Longyi/DynamicMenu/src/",
+            "Longyi\\Email\\": "packages/Longyi/Email/src/",
             "Longyi\\RewardPoints\\": "packages/Longyi/RewardPoints/src/",
             "Longyi\\Member\\": "packages/Longyi/Member/src/",
             "Longyi\\Gift\\": "packages/Longyi/Gift/src/",

+ 29 - 0
packages/Longyi/DynamicMenu/src/Database/Migrations/add_route_parameters_to_menu_items.php

@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('dynamic_menu_items', function (Blueprint $table) {
+            $table->text('route_parameters')->nullable()->after('route')
+                ->comment('路由参数,JSON格式存储,如:["emails","configure"]');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('dynamic_menu_items', function (Blueprint $table) {
+            $table->dropColumn('route_parameters');
+        });
+    }
+};

+ 148 - 43
packages/Longyi/DynamicMenu/src/Http/Controllers/MenuController.php

@@ -8,18 +8,19 @@ use Longyi\DynamicMenu\Models\MenuItem;
 use Webkul\User\Repositories\RoleRepository;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\View;
+use Illuminate\Support\Facades\Route as RouteFacade;
 
 class MenuController extends Controller
 {
     protected $roleRepository;
-    
+
     public function __construct(RoleRepository $roleRepository)
     {
         $this->roleRepository = $roleRepository;
         $this->_config = request('_config');
     }
-    
-    
+
+
     public function index()
     {
         try {
@@ -28,34 +29,34 @@ class MenuController extends Controller
                 ->orderBy('parent_id', 'ASC')
                 ->orderBy('sort_order', 'ASC')
                 ->get();
-            
+
             // 获取顶级菜单项
-             $menuItems = $allItems->filter(function($item) {
+            $menuItems = $allItems->filter(function($item) {
                 return is_null($item->parent_id) || $item->parent_id == 0;
             })->values();
-            
-            
+
+
             return view('dynamicmenu::admin.menu.index', compact('menuItems', 'allItems'));
-            
+
         } catch (\Exception $e) {
             \Log::error('MenuController 错误:' . $e->getMessage());
             \Log::error($e->getTraceAsString());
-            
+
             session()->flash('error', '加载菜单列表失败:' . $e->getMessage());
             return redirect()->route('admin.dashboard.index');
         }
     }
-    
-    
+
+
     public function create()
     {
         $menuItems = MenuItem::orderBy('parent_id')->orderBy('sort_order')->get();
         return view($this->_config['view'], compact('menuItems'));
     }
-    
+
     public function store(Request $request)
     {
-        
+
         $request->validate([
             'name' => 'required|string|max:255',
             'key' => 'required|string|unique:dynamic_menu_items,key',
@@ -65,32 +66,32 @@ class MenuController extends Controller
             'sort_order' => 'integer',
             'status' => 'boolean'
         ]);
-        
+
         $data = $request->all();
         $data['parent_id'] = $data['parent_id'] === '' ? null : $data['parent_id'];
         $data['created_by'] = auth()->guard('admin')->user()->id;
-        
+
         MenuItem::create($data);
-        
+
         session()->flash('success', '菜单项创建成功');
-        
+
         return redirect()->route($this->_config['redirect']);
     }
-    
+
     public function edit($id)
     {
         try {
             $menuItem = MenuItem::with('parent')->findOrFail($id);
             $menuItems = MenuItem::orderBy('parent_id')->orderBy('sort_order')->get();
-            
+
             return view($this->_config['view'], compact('menuItem', 'menuItems'));
-            
+
         } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
             session()->flash('error', '菜单项不存在');
             return redirect()->route('admin.dynamicmenu.index');
         }
     }
-    
+
     public function update(Request $request, $id)
     {
         try {
@@ -103,14 +104,14 @@ class MenuController extends Controller
                 'sort_order' => 'integer',
                 'status' => 'boolean'
             ]);
-            
+
             $menuItem = MenuItem::findOrFail($id);
             $menuItem->update($request->all());
-            
+
             session()->flash('success', '菜单项更新成功');
-            
+
             return redirect()->route($this->_config['redirect']);
-            
+
         } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
             session()->flash('error', '菜单项不存在');
             return redirect()->back();
@@ -120,24 +121,24 @@ class MenuController extends Controller
             return redirect()->back()->withInput();
         }
     }
-    
-     public function destroy($id)
+
+    public function destroy($id)
     {
         try {
             $menuItem = MenuItem::with('children')->findOrFail($id);
-            
+
             // 检查是否有子菜单
             if ($menuItem->children && $menuItem->children->count() > 0) {
                 session()->flash('error', '请先删除子菜单项');
                 return redirect()->back();
             }
-            
+
             $menuItem->delete();
-            
+
             session()->flash('success', '菜单项删除成功');
-            
+
             return redirect()->route($this->_config['redirect']);
-            
+
         } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
             session()->flash('error', '菜单项不存在');
             return redirect()->back();
@@ -147,43 +148,147 @@ class MenuController extends Controller
             return redirect()->back();
         }
     }
-    
+
     public function permission()
     {
         $roles = $this->roleRepository->all();
         $menuItems = MenuItem::with('children')->orderBy('parent_id')->orderBy('sort_order')->get();
-        
+
         return view($this->_config['view'], compact('roles', 'menuItems'));
     }
-    
+
     public function updatePermission(Request $request)
     {
         $roleId = $request->input('role_id');
         $menuItemIds = $request->input('menu_items', []);
-        
+
         DB::table('menu_item_role')->where('role_id', $roleId)->delete();
-        
+
         foreach ($menuItemIds as $menuItemId) {
             DB::table('menu_item_role')->insert([
                 'menu_item_id' => $menuItemId,
                 'role_id' => $roleId
             ]);
         }
-        
+
         session()->flash('success', '权限更新成功');
-        
+
         return redirect()->back();
     }
-    
+
     public function getRolePermissions(Request $request)
     {
         $roleId = $request->input('role_id');
-        
+
         $permissions = DB::table('menu_item_role')
             ->where('role_id', $roleId)
             ->pluck('menu_item_id')
             ->toArray();
-            
+
         return response()->json($permissions);
     }
-}
+
+    /**
+     * 验证路由是否存在
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function validateRoute(Request $request)
+    {
+        $request->validate([
+            'route' => 'required|string'
+        ]);
+
+        $routeName = trim($request->input('route'));
+
+        // 如果是完整URL(以http://或https://开头),跳过验证
+        if (preg_match('/^https?:\/\//i', $routeName)) {
+            return response()->json([
+                'exists' => true,
+                'message' => '外部URL,已跳过验证'
+            ]);
+        }
+
+        // 如果是相对路径(以/开头),跳过验证
+        if (str_starts_with($routeName, '/')) {
+            return response()->json([
+                'exists' => true,
+                'message' => '相对路径,已跳过验证'
+            ]);
+        }
+
+        // 检查路由是否存在
+        try {
+            $route = RouteFacade::getRoutes()->getByName($routeName);
+
+            if ($route) {
+                return response()->json([
+                    'exists' => true,
+                    'message' => "路由 '{$routeName}' 存在",
+                    'methods' => $route->methods(),
+                    'uri' => $route->uri()
+                ]);
+            } else {
+                // 尝试查找相似的路由名称,提供建议
+                $suggestions = $this->findSimilarRoutes($routeName);
+
+                return response()->json([
+                    'exists' => false,
+                    'message' => "路由 '{$routeName}' 不存在",
+                    'suggestions' => $suggestions
+                ], 422);
+            }
+        } catch (\Exception $e) {
+            \Log::error('Route validation error: ' . $e->getMessage());
+
+            return response()->json([
+                'exists' => false,
+                'message' => '路由验证失败:' . $e->getMessage()
+            ], 500);
+        }
+    }
+
+    /**
+     * 查找相似的路由名称
+     *
+     * @param string $routeName
+     * @return array
+     */
+    protected function findSimilarRoutes(string $routeName): array
+    {
+        $allRoutes = RouteFacade::getRoutes();
+        $similarRoutes = [];
+
+        // 提取路由的主要部分
+        $parts = explode('.', $routeName);
+        $mainPart = $parts[0] ?? '';
+
+        foreach ($allRoutes as $route) {
+            $name = $route->getName();
+
+            if (!$name) {
+                continue;
+            }
+
+            // 计算相似度
+            similar_text($routeName, $name, $percent);
+
+            // 如果相似度超过60%,或者包含相同的前缀
+            if ($percent > 60 || (isset($parts[0]) && str_contains($name, $parts[0]))) {
+                $similarRoutes[] = [
+                    'name' => $name,
+                    'uri' => $route->uri(),
+                    'similarity' => round($percent, 2)
+                ];
+            }
+        }
+
+        // 按相似度排序,取前5个
+        usort($similarRoutes, function($a, $b) {
+            return $b['similarity'] <=> $a['similarity'];
+        });
+
+        return array_slice($similarRoutes, 0, 5);
+    }
+}

+ 7 - 6
packages/Longyi/DynamicMenu/src/Models/MenuItem.php

@@ -7,18 +7,19 @@ use Illuminate\Support\Facades\Cache;
 class MenuItem extends Model
 {
     protected $table = 'dynamic_menu_items';
-    
+
     protected $fillable = [
         'name',
         'key',
         'route',
+        'route_parameters',
         'icon',
         'parent_id',
         'sort_order',
         'status',
         'created_by'
     ];
-    
+
     protected $casts = [
         'status' => 'boolean',
         'sort_order' => 'integer'
@@ -26,14 +27,14 @@ class MenuItem extends Model
     protected static function boot()
     {
         parent::boot();
-        
+
         // 当菜单项被创建、更新或删除时,清除缓存
         static::saved(function () {
             Cache::forget('dynamic_menu_config');
             Cache::forget('dynamic_menu_items');
             \Log::info('菜单缓存已清除 (saved)');
         });
-        
+
         static::deleted(function () {
             Cache::forget('dynamic_menu_config');
             Cache::forget('dynamic_menu_items');
@@ -48,7 +49,7 @@ class MenuItem extends Model
     {
         return $this->belongsTo(MenuItem::class, 'parent_id');
     }
-    
+
     /**
      * 获取子菜单
      */
@@ -56,4 +57,4 @@ class MenuItem extends Model
     {
         return $this->hasMany(MenuItem::class, 'parent_id')->orderBy('sort_order');
     }
-}
+}

+ 38 - 0
packages/Longyi/DynamicMenu/src/Providers/DynamicMenuServiceProvider.php

@@ -51,6 +51,7 @@ class DynamicMenuServiceProvider extends ServiceProvider
             $existingConfig = $this->app['config']->get('menu.admin', []);
             // 合并配置
             $mergedConfig = array_merge($existingConfig, $menuConfig);
+
             // 设置配置
             $this->app['config']->set('menu.admin', $mergedConfig);
 
@@ -102,10 +103,13 @@ class DynamicMenuServiceProvider extends ServiceProvider
         $config = [];
 
         foreach ($menuItems as $item) {
+            $routeParameters = $this->parseRouteParameters($item->route_parameters);
+
             $menuItem = [
                 'key'   => $item->key,
                 'name'  => $item->name,
                 'route' => $item->route ?: 'admin.dynamicmenu.index',
+                'route_parameters' => $routeParameters,
                 'sort'  => (int) $item->sort_order,
                 'icon'  => $item->icon ?: 'icon-menu',
             ];
@@ -114,4 +118,38 @@ class DynamicMenuServiceProvider extends ServiceProvider
 
         return $config;
     }
+
+    /**
+     * 解析路由参数
+     */
+    protected function parseRouteParameters($parameters): array
+    {
+        if (empty($parameters)) {
+            return [];
+        }
+
+        // 如果已经是数组,直接返回
+        if (is_array($parameters)) {
+            return array_values(array_filter($parameters));
+        }
+
+        // 如果是 JSON 字符串,解码
+        if (is_string($parameters)) {
+            // 尝试 JSON 解码
+            $decoded = json_decode($parameters, true);
+            if (is_array($decoded)) {
+                return array_values(array_filter($decoded));
+            }
+
+            // 如果不是有效的 JSON,尝试按逗号分割
+            if (strpos($parameters, ',') !== false) {
+                return array_values(array_filter(array_map('trim', explode(',', $parameters))));
+            }
+
+            // 单个字符串参数
+            return [$parameters];
+        }
+
+        return [];
+    }
 }

+ 227 - 79
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/create.blade.php

@@ -34,7 +34,7 @@
             </div>
 
             <div class="panel-body" style="padding: 20px;">
-                <form method="POST" action="{{ route('admin.dynamicmenu.store') }}">
+                <form method="POST" action="{{ route('admin.dynamicmenu.store') }}" id="menuForm">
                     @csrf
 
                     {{-- 名称 --}}
@@ -44,6 +44,7 @@
                         </label>
                         <input type="text"
                                name="name"
+                               id="nameInput"
                                class="control"
                                value="{{ old('name') }}"
                                required
@@ -57,16 +58,16 @@
                             Key <span style="color: red;">*</span>
                         </label>
                         <input type="text"
-                            name="key"
-                            class="control"
-                            value="{{ old('key') }}"
-                            required
-                            placeholder="例如:settings.dynamic-menu"
-                            style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                               name="key"
+                               id="keyInput"
+                               class="control"
+                               value="{{ old('key') }}"
+                               required
+                               placeholder="例如:settings.dynamic-menu"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
                         <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
                             <strong style="color: #dc3545;">重要提示:</strong>
-                            如果想让菜单显示在 Settings 下,Key 必须以 <strong style="background: #f0f0f0; padding: 2px 5px; border-radius: 3px;">settings.</strong> 开头<br>
-                            例如:<code>settings.dynamic-menu</code>、<code>settings.my-menu</code>、<code>settings.custom</code>
+                            如果想让菜单显示在 Settings 下,Key 必须以 <strong>settings.</strong> 开头
                         </small>
                     </div>
 
@@ -75,13 +76,15 @@
                         <label style="display: block; margin-bottom: 5px; font-weight: 500;">URL/路由</label>
                         <input type="text"
                                name="route"
+                               id="routeInput"
                                class="control"
                                value="{{ old('route') }}"
                                placeholder="例如:admin.dashboard"
                                style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
                         <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
-                            可以是相对路径、完整URL或路由名称
+                            可以是路由名称(如:admin.dashboard)或完整URL
                         </small>
+                        <div id="routeValidationResult" style="margin-top: 5px; font-size: 12px;"></div>
                     </div>
 
                     {{-- 图标 --}}
@@ -93,9 +96,6 @@
                                value="{{ old('icon', 'fas fa-file') }}"
                                placeholder="例如:fas fa-dashboard、fas fa-users等"
                                style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
-                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
-                            FontAwesome图标类名,如:fas fa-home
-                        </small>
                     </div>
 
                     {{-- 父级菜单 --}}
@@ -123,9 +123,6 @@
                                value="{{ old('sort_order', 0) }}"
                                min="0"
                                style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
-                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
-                            数字越小越靠前
-                        </small>
                     </div>
 
                     {{-- 状态 --}}
@@ -157,68 +154,219 @@
         </div>
     </div>
 
-    {{-- 引入必要的JavaScript --}}
-@push('scripts')
-<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
-<script>
-   /* $(document).ready(function() {
-        // 自动生成key
-        $('input[name="name"]').on('blur', function() {
-            var name = $(this).val();
-            var keyInput = $('input[name="key"]');
-
-            if (keyInput.val() === '') {
-                // 将名称转换为小写,空格替换为横线,移除特殊字符
-                var key = name.toLowerCase()
-                             .replace(/\s+/g, '-')
-                             .replace(/[^a-z0-9-]/g, '');
-                // 默认添加到 settings 下
-                keyInput.val('settings.' + key);
-
-                // 添加一个视觉提示
-                keyInput.css('border-color', '#28a745');
-                setTimeout(function() {
-                    keyInput.css('border-color', '');
-                }, 1000);
-
-                // 显示成功提示
-                var successMsg = $('<small class="key-success" style="display: block; margin-top: 5px; font-size: 12px; color: #28a745;">' +
-                                 '✓ 已自动生成Key: settings.' + key + '</small>');
-                keyInput.after(successMsg);
-                setTimeout(function() {
-                    $('.key-success').remove();
-                }, 3000);
-            }
-        });
-
-        // 为所有表单元素添加Bagisto样式类
-        $('.control').addClass('form-control');
-
-        // 检查key格式
-        $('form').on('submit', function(e) {
-            var keyValue = $('input[name="key"]').val();
-            if (keyValue && keyValue.indexOf('settings.') !== 0) {
-                if (!confirm('当前Key不以"settings."开头,菜单可能不会显示在Settings下。确定要继续吗?')) {
-                    e.preventDefault();
+    {{-- JavaScript 代码 - 使用事件委托 --}}
+    @push('scripts')
+        <script>
+            (function() {
+                console.log('DynamicMenu: Script loaded');
+
+                // 使用 setInterval 等待元素出现(因为 Bagisto 可能会延迟渲染)
+                let checkInterval = setInterval(function() {
+                    const routeInput = document.getElementById('routeInput');
+                    if (routeInput) {
+                        clearInterval(checkInterval);
+                        console.log('DynamicMenu: routeInput found!');
+                        initValidation(routeInput);
+                    } else {
+                        console.log('DynamicMenu: waiting for routeInput...');
+                    }
+                }, 100);
+
+                function initValidation(routeInput) {
+                    const validationResult = document.getElementById('routeValidationResult');
+                    const menuForm = document.getElementById('menuForm');
+                    const nameInput = document.getElementById('nameInput');
+                    const keyInput = document.getElementById('keyInput');
+
+                    let validationTimeout = null;
+                    let lastValidatedRoute = '';
+                    let routeIsValid = false;
+
+                    function getCsrfToken() {
+                        const tokenMeta = document.querySelector('meta[name="csrf-token"]');
+                        if (tokenMeta) return tokenMeta.getAttribute('content');
+                        const tokenInput = document.querySelector('input[name="_token"]');
+                        if (tokenInput) return tokenInput.value;
+                        return '';
+                    }
+
+                    function clearValidation() {
+                        if (routeInput) {
+                            routeInput.style.borderColor = '';
+                            routeInput.style.backgroundColor = '';
+                        }
+                        if (validationResult) {
+                            validationResult.innerHTML = '';
+                            validationResult.style.display = 'none';
+                        }
+                        routeIsValid = false;
+                        lastValidatedRoute = '';
+                    }
+
+                    function showLoadingState() {
+                        if (routeInput) {
+                            routeInput.style.borderColor = '#ffc107';
+                            routeInput.style.backgroundColor = '#fffbeb';
+                        }
+                        if (validationResult) {
+                            validationResult.innerHTML = '<span style="color: #ffc107;">⏳ 正在验证路由...</span>';
+                            validationResult.style.display = 'block';
+                        }
+                    }
+
+                    function showSuccessState(message) {
+                        if (routeInput) {
+                            routeInput.style.borderColor = '#28a745';
+                            routeInput.style.backgroundColor = '#f0fff4';
+                        }
+                        if (validationResult) {
+                            validationResult.innerHTML = '<span style="color: #28a745;">✓ ' + message + '</span>';
+                            validationResult.style.display = 'block';
+                        }
+                        setTimeout(() => {
+                            if (routeInput) routeInput.style.backgroundColor = '';
+                        }, 3000);
+                    }
+
+                    function showErrorState(message) {
+                        if (routeInput) {
+                            routeInput.style.borderColor = '#dc3545';
+                            routeInput.style.backgroundColor = '#fff5f5';
+                        }
+                        if (validationResult) {
+                            validationResult.innerHTML = '<span style="color: #dc3545;">✗ ' + message + '</span>';
+                            validationResult.style.display = 'block';
+                        }
+                    }
+
+                    function validateRoute(routeName) {
+                        console.log('Validating route:', routeName);
+
+                        if (!routeName) {
+                            clearValidation();
+                            return;
+                        }
+
+                        showLoadingState();
+
+                        const csrfToken = getCsrfToken();
+                        const validateUrl = '{{ route("admin.dynamicmenu.validate-route") }}';
+
+                        fetch(validateUrl, {
+                            method: 'POST',
+                            headers: {
+                                'Content-Type': 'application/json',
+                                'X-CSRF-TOKEN': csrfToken,
+                                'Accept': 'application/json'
+                            },
+                            body: JSON.stringify({ route: routeName })
+                        })
+                            .then(response => response.json())
+                            .then(data => {
+                                console.log('Validation response:', data);
+                                if (data.exists) {
+                                    routeIsValid = true;
+                                    showSuccessState(data.message);
+                                } else {
+                                    routeIsValid = false;
+                                    let errorMsg = data.message || '路由不存在';
+                                    if (data.suggestions && data.suggestions.length > 0) {
+                                        errorMsg += '<br>建议使用:<br>';
+                                        data.suggestions.forEach(s => {
+                                            errorMsg += `&nbsp;&nbsp;• ${s.name} (${s.uri})<br>`;
+                                        });
+                                    }
+                                    showErrorState(errorMsg);
+                                }
+                                lastValidatedRoute = routeName;
+                            })
+                            .catch(error => {
+                                console.error('Validation error:', error);
+                                routeIsValid = false;
+                                showErrorState('验证失败,请稍后重试');
+                            });
+                    }
+
+                    // 使用事件委托(通过 document.body)
+                    document.body.addEventListener('input', function(e) {
+                        if (e.target.id === 'routeInput') {
+                            console.log('Input event on routeInput:', e.target.value);
+                            const routeValue = e.target.value.trim();
+
+                            if (validationTimeout) clearTimeout(validationTimeout);
+
+                            if (!routeValue) {
+                                clearValidation();
+                                return;
+                            }
+
+                            validationTimeout = setTimeout(() => {
+                                validateRoute(routeValue);
+                            }, 500);
+                        }
+                    });
+
+                    document.body.addEventListener('blur', function(e) {
+                        if (e.target.id === 'routeInput') {
+                            console.log('Blur event on routeInput:', e.target.value);
+                            const routeValue = e.target.value.trim();
+                            if (routeValue && routeValue !== lastValidatedRoute) {
+                                validateRoute(routeValue);
+                            }
+                        }
+                    });
+
+                    // 自动生成 key
+                    if (nameInput && keyInput) {
+                        nameInput.addEventListener('blur', function() {
+                            const name = this.value.trim();
+                            const key = keyInput.value.trim();
+
+                            if (key === '' && name !== '') {
+                                let generatedKey = name.toLowerCase()
+                                    .replace(/\s+/g, '-')
+                                    .replace(/[^a-z0-9-]/g, '');
+
+                                if (generatedKey) {
+                                    keyInput.value = 'settings.' + generatedKey;
+                                    keyInput.style.borderColor = '#28a745';
+                                    setTimeout(function() {
+                                        keyInput.style.borderColor = '';
+                                    }, 3000);
+                                }
+                            }
+                        });
+                    }
+
+                    // 表单提交验证
+                    if (menuForm) {
+                        menuForm.addEventListener('submit', function(e) {
+                            const routeValue = routeInput.value.trim();
+
+                            if (routeValue) {
+                                const isLoading = validationResult && validationResult.innerHTML.includes('正在验证');
+                                const hasError = validationResult && validationResult.innerHTML.includes('✗');
+
+                                if (isLoading) {
+                                    e.preventDefault();
+                                    alert('正在验证路由,请稍候...');
+                                    return false;
+                                }
+
+                                if (hasError) {
+                                    e.preventDefault();
+                                    routeInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
+                                    routeInput.focus();
+                                    alert('路由不存在,请修正后再提交!');
+                                    return false;
+                                }
+                            }
+                        });
+                    }
+
+                    console.log('DynamicMenu: Validation initialized successfully');
                 }
-            }
-        });
-
-        // 添加提示
-        $('input[name="key"]').on('focus', function() {
-            var currentVal = $(this).val();
-            if (currentVal && currentVal.indexOf('settings.') !== 0) {
-                $(this).after(
-                    '<small class="key-warning" style="display: block; margin-top: 5px; font-size: 12px; color: #dc3545;">' +
-                    '⚠️ 警告:当前Key不以"settings."开头,菜单可能不会显示在Settings下</small>'
-                );
-
-                setTimeout(function() {
-                    $('.key-warning').remove();
-                }, 3000);
-            }
-        });
-    });*/
-</script>
-@endpush
+            })();
+        </script>
+    @endpush
 </x-admin::layouts>

+ 275 - 37
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/edit.blade.php

@@ -34,7 +34,7 @@
             </div>
 
             <div class="panel-body" style="padding: 20px;">
-                <form method="POST" action="{{ route('admin.dynamicmenu.update', $menuItem->id) }}">
+                <form method="POST" action="{{ route('admin.dynamicmenu.update', $menuItem->id) }}" id="menuForm">
                     @csrf
                     @method('PUT')
 
@@ -45,6 +45,7 @@
                         </label>
                         <input type="text"
                                name="name"
+                               id="nameInput"
                                class="control"
                                value="{{ old('name', $menuItem->name) }}"
                                required
@@ -53,16 +54,21 @@
                     </div>
 
                     {{-- Key 字段 --}}
-                    <div class="form-group">
-                        <label class="required">Key</label>
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label class="required" style="display: block; margin-bottom: 5px; font-weight: 500;">
+                            Key <span style="color: red;">*</span>
+                        </label>
                         <input type="text"
-                            name="key"
-                            class="control"
-                            value="{{ old('key', $menuItem->key ?? '') }}"
-                            required
-                            placeholder="例如:settings.my-menu">
-                        <small class="control-info">
-                            建议以 'settings.' 开头,这样菜单会显示在 Settings 下。例如:settings.dynamic-menu
+                               name="key"
+                               id="keyInput"
+                               class="control"
+                               value="{{ old('key', $menuItem->key ?? '') }}"
+                               required
+                               placeholder="例如:settings.my-menu"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            <strong style="color: #dc3545;">重要提示:</strong>
+                            如果想让菜单显示在 Settings 下,Key 必须以 <strong>settings.</strong> 开头。例如:settings.dynamic-menu
                         </small>
                     </div>
 
@@ -71,13 +77,15 @@
                         <label style="display: block; margin-bottom: 5px; font-weight: 500;">URL/路由</label>
                         <input type="text"
                                name="route"
+                               id="routeInput"
                                class="control"
                                value="{{ old('route', $menuItem->route) }}"
                                placeholder="例如:admin.dashboard"
                                style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
                         <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
-                            可以是相对路径、完整URL或路由名称
+                            可以是路由名称(如:admin.dashboard)或完整URL
                         </small>
+                        <div id="routeValidationResult" style="margin-top: 5px; font-size: 12px;"></div>
                     </div>
 
                     {{-- 图标 --}}
@@ -97,16 +105,16 @@
                     {{-- 父级菜单 --}}
                     <div class="form-group" style="margin-bottom: 20px;">
                         <label style="display: block; margin-bottom: 5px; font-weight: 500;">父级菜单</label>
-                        <select name="parent_id" class="control" style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <select name="parent_id" id="parentSelect" class="control" style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
                             <option value="">作为顶级菜单</option>
                             @foreach($menuItems as $menuItemOption)
-                                @if($menuItemOption->id != $menuItem->id) {{-- 不能选择自己作为父级 --}}
-                                <option value="{{ $menuItemOption->id }}" {{ old('parent_id', $menuItem->parent_id) == $menuItemOption->id ? 'selected' : '' }}>
-                                    {{ $menuItemOption->name }}
-                                    @if($menuItemOption->children && $menuItemOption->children->count() > 0)
-                                        (有子菜单)
-                                    @endif
-                                </option>
+                                @if($menuItemOption->id != $menuItem->id)
+                                    <option value="{{ $menuItemOption->id }}" {{ old('parent_id', $menuItem->parent_id) == $menuItemOption->id ? 'selected' : '' }}>
+                                        {{ $menuItemOption->name }}
+                                        @if($menuItemOption->children && $menuItemOption->children->count() > 0)
+                                            (有子菜单)
+                                        @endif
+                                    </option>
                                 @endif
                             @endforeach
                         </select>
@@ -155,25 +163,255 @@
         </div>
     </div>
 
-    {{-- 引入必要的JavaScript --}}
+    {{-- JavaScript 代码 - 使用事件委托 --}}
     @push('scripts')
-    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
-    <script>
-        /*$(document).ready(function() {
-            // 自动生成key
-            $('input[name="name"]').on('blur', function() {
-                var name = $(this).val();
-                var keyInput = $('input[name="key"]');
-
-                if (keyInput.val() === '') {
-                    var key = name.toLowerCase()
-                                .replace(/\s+/g, '-')
-                                .replace(/[^a-z0-9-]/g, '');
-                    // 默认添加到 settings 下
-                    keyInput.val('settings.' + key);
+        <script>
+            (function() {
+                console.log('DynamicMenu Edit: Script loaded');
+
+                // 等待元素出现
+                let checkInterval = setInterval(function() {
+                    const routeInput = document.getElementById('routeInput');
+                    if (routeInput) {
+                        clearInterval(checkInterval);
+                        console.log('DynamicMenu Edit: routeInput found!');
+                        initValidation(routeInput);
+                    }
+                }, 100);
+
+                function initValidation(routeInput) {
+                    const validationResult = document.getElementById('routeValidationResult');
+                    const menuForm = document.getElementById('menuForm');
+                    const nameInput = document.getElementById('nameInput');
+                    const keyInput = document.getElementById('keyInput');
+
+                    let validationTimeout = null;
+                    let lastValidatedRoute = '';
+                    let routeIsValid = false;
+
+                    function getCsrfToken() {
+                        const tokenMeta = document.querySelector('meta[name="csrf-token"]');
+                        if (tokenMeta) return tokenMeta.getAttribute('content');
+                        const tokenInput = document.querySelector('input[name="_token"]');
+                        if (tokenInput) return tokenInput.value;
+                        return '';
+                    }
+
+                    function showTemporaryMessage(container, message, color) {
+                        const existingMsg = container.querySelector('.temp-msg');
+                        if (existingMsg) existingMsg.remove();
+
+                        const msgDiv = document.createElement('div');
+                        msgDiv.className = 'temp-msg';
+                        msgDiv.style.cssText = `display: block; margin-top: 5px; font-size: 12px; color: ${color};`;
+                        msgDiv.innerHTML = message;
+                        container.appendChild(msgDiv);
+
+                        setTimeout(() => {
+                            if (msgDiv.parentNode) msgDiv.remove();
+                        }, 3000);
+                    }
+
+                    function clearValidation() {
+                        if (routeInput) {
+                            routeInput.style.borderColor = '';
+                            routeInput.style.backgroundColor = '';
+                        }
+                        if (validationResult) {
+                            validationResult.innerHTML = '';
+                            validationResult.style.display = 'none';
+                        }
+                        routeIsValid = false;
+                        lastValidatedRoute = '';
+                    }
+
+                    function showLoadingState() {
+                        if (routeInput) {
+                            routeInput.style.borderColor = '#ffc107';
+                            routeInput.style.backgroundColor = '#fffbeb';
+                        }
+                        if (validationResult) {
+                            validationResult.innerHTML = '<span style="color: #ffc107;">⏳ 正在验证路由...</span>';
+                            validationResult.style.display = 'block';
+                        }
+                    }
+
+                    function showSuccessState(message) {
+                        if (routeInput) {
+                            routeInput.style.borderColor = '#28a745';
+                            routeInput.style.backgroundColor = '#f0fff4';
+                        }
+                        if (validationResult) {
+                            validationResult.innerHTML = '<span style="color: #28a745;">✓ ' + message + '</span>';
+                            validationResult.style.display = 'block';
+                        }
+                        setTimeout(() => {
+                            if (routeInput) routeInput.style.backgroundColor = '';
+                        }, 3000);
+                    }
+
+                    function showErrorState(message) {
+                        if (routeInput) {
+                            routeInput.style.borderColor = '#dc3545';
+                            routeInput.style.backgroundColor = '#fff5f5';
+                        }
+                        if (validationResult) {
+                            validationResult.innerHTML = '<span style="color: #dc3545;">✗ ' + message + '</span>';
+                            validationResult.style.display = 'block';
+                        }
+                    }
+
+                    function validateRoute(routeName) {
+                        console.log('Validating route:', routeName);
+
+                        if (!routeName) {
+                            clearValidation();
+                            return;
+                        }
+
+                        showLoadingState();
+
+                        const csrfToken = getCsrfToken();
+                        const validateUrl = '{{ route("admin.dynamicmenu.validate-route") }}';
+
+                        fetch(validateUrl, {
+                            method: 'POST',
+                            headers: {
+                                'Content-Type': 'application/json',
+                                'X-CSRF-TOKEN': csrfToken,
+                                'Accept': 'application/json'
+                            },
+                            body: JSON.stringify({ route: routeName })
+                        })
+                            .then(response => response.json())
+                            .then(data => {
+                                console.log('Validation response:', data);
+                                if (data.exists) {
+                                    routeIsValid = true;
+                                    showSuccessState(data.message);
+                                } else {
+                                    routeIsValid = false;
+                                    let errorMsg = data.message || '路由不存在';
+                                    if (data.suggestions && data.suggestions.length > 0) {
+                                        errorMsg += '<br>建议使用:<br>';
+                                        data.suggestions.forEach(s => {
+                                            errorMsg += `&nbsp;&nbsp;• ${s.name} (${s.uri})<br>`;
+                                        });
+                                    }
+                                    showErrorState(errorMsg);
+                                }
+                                lastValidatedRoute = routeName;
+                            })
+                            .catch(error => {
+                                console.error('Validation error:', error);
+                                routeIsValid = false;
+                                showErrorState('验证失败,请稍后重试');
+                            });
+                    }
+
+                    // 使用事件委托
+                    document.body.addEventListener('input', function(e) {
+                        if (e.target.id === 'routeInput') {
+                            console.log('Input event on routeInput:', e.target.value);
+                            const routeValue = e.target.value.trim();
+
+                            if (validationTimeout) clearTimeout(validationTimeout);
+
+                            if (!routeValue) {
+                                clearValidation();
+                                return;
+                            }
+
+                            validationTimeout = setTimeout(() => {
+                                validateRoute(routeValue);
+                            }, 500);
+                        }
+                    });
+
+                    document.body.addEventListener('blur', function(e) {
+                        if (e.target.id === 'routeInput') {
+                            console.log('Blur event on routeInput:', e.target.value);
+                            const routeValue = e.target.value.trim();
+                            if (routeValue && routeValue !== lastValidatedRoute) {
+                                validateRoute(routeValue);
+                            }
+                        }
+                    });
+
+                    // 自动生成 key(仅当 key 为空时)
+                    if (nameInput && keyInput) {
+                        nameInput.addEventListener('blur', function() {
+                            const name = this.value.trim();
+                            const key = keyInput.value.trim();
+
+                            if (key === '' && name !== '') {
+                                let generatedKey = name.toLowerCase()
+                                    .replace(/\s+/g, '-')
+                                    .replace(/[^a-z0-9-]/g, '');
+
+                                if (generatedKey) {
+                                    keyInput.value = 'settings.' + generatedKey;
+                                    keyInput.style.borderColor = '#28a745';
+                                    showTemporaryMessage(keyInput.parentElement, '✓ 已自动生成Key: settings.' + generatedKey, '#28a745');
+                                    setTimeout(() => {
+                                        keyInput.style.borderColor = '';
+                                    }, 3000);
+                                }
+                            }
+                        });
+                    }
+
+                    // Key 格式提示
+                    if (keyInput) {
+                        keyInput.addEventListener('focus', function() {
+                            const currentVal = this.value;
+                            if (currentVal && !currentVal.startsWith('settings.')) {
+                                showTemporaryMessage(this.parentElement,
+                                    '⚠️ 警告:当前Key不以"settings."开头,菜单可能不会显示在Settings下',
+                                    '#dc3545');
+                            }
+                        });
+                    }
+
+                    // 表单提交验证
+                    if (menuForm) {
+                        menuForm.addEventListener('submit', function(e) {
+                            const routeValue = routeInput.value.trim();
+
+                            if (routeValue) {
+                                const isLoading = validationResult && validationResult.innerHTML.includes('正在验证');
+                                const hasError = validationResult && validationResult.innerHTML.includes('✗');
+
+                                if (isLoading) {
+                                    e.preventDefault();
+                                    alert('正在验证路由,请稍候...');
+                                    return false;
+                                }
+
+                                if (hasError) {
+                                    e.preventDefault();
+                                    routeInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
+                                    routeInput.focus();
+                                    alert('路由不存在,请修正后再提交!\n\n提示:\n' +
+                                        '1. 检查路由名称是否正确\n' +
+                                        '2. 确保路由已在 routes/web.php 中定义\n' +
+                                        '3. 可以使用完整URL(如:https://example.com)跳过验证');
+                                    return false;
+                                }
+                            }
+                        });
+                    }
+
+                    // 如果有初始值,验证一下
+                    if (routeInput.value.trim()) {
+                        setTimeout(() => {
+                            validateRoute(routeInput.value.trim());
+                        }, 500);
+                    }
+
+                    console.log('DynamicMenu Edit: Validation initialized successfully');
                 }
-            });
-        });*/
-    </script>
+            })();
+        </script>
     @endpush
 </x-admin::layouts>

+ 16 - 12
packages/Longyi/DynamicMenu/src/Routes/admin-routes.php

@@ -5,53 +5,57 @@ use Longyi\DynamicMenu\Http\Controllers\MenuController;
 
 // 关键:使用正确的路由组
 Route::group(['middleware' => ['web', 'admin'], 'prefix' => 'admin'], function () {
-    
-    
-    
+
+
+
     // 菜单管理路由 - 注意这里不要重复加 'admin' 前缀
     Route::get('dynamicmenu', [MenuController::class, 'index'])
         ->name('admin.dynamicmenu.index')
         ->defaults('_config', [
             'view' => 'dynamicmenu::admin.menu.index'
         ]);
-    
+
     Route::get('dynamicmenu/create', [MenuController::class, 'create'])
         ->name('admin.dynamicmenu.create')
         ->defaults('_config', [
             'view' => 'dynamicmenu::admin.menu.create'
         ]);
-    
+
     Route::post('dynamicmenu', [MenuController::class, 'store'])
         ->name('admin.dynamicmenu.store')
         ->defaults('_config', [
             'redirect' => 'admin.dynamicmenu.index'
         ]);
-    
+
     Route::get('dynamicmenu/{id}/edit', [MenuController::class, 'edit'])
         ->name('admin.dynamicmenu.edit')
         ->defaults('_config', [
             'view' => 'dynamicmenu::admin.menu.edit'
         ]);
-    
+
     Route::put('dynamicmenu/{id}', [MenuController::class, 'update'])
         ->name('admin.dynamicmenu.update')
         ->defaults('_config', [
             'redirect' => 'admin.dynamicmenu.index'
         ]);
-    
+
     Route::delete('dynamicmenu/{id}', [MenuController::class, 'destroy'])
         ->name('admin.dynamicmenu.delete');
-    
+
     // 权限管理路由
     Route::get('dynamicmenu-permissions', [MenuController::class, 'permission'])
         ->name('admin.dynamicmenu.permission')
         ->defaults('_config', [
             'view' => 'dynamicmenu::admin.permission.index'
         ]);
-    
+
     Route::post('dynamicmenu-permissions', [MenuController::class, 'updatePermission'])
         ->name('admin.dynamicmenu.permission.update');
-    
+
     Route::post('dynamicmenu-permissions/get', [MenuController::class, 'getRolePermissions'])
         ->name('admin.dynamicmenu.permission.get');
-});
+
+    // 路由验证接口
+    Route::post('dynamicmenu/validate-route', [MenuController::class, 'validateRoute'])
+        ->name('admin.dynamicmenu.validate-route');
+});

+ 34 - 0
packages/Longyi/Email/README.md

@@ -0,0 +1,34 @@
+### 依赖关系
+
+- **PHP**: ^8.1|^8.2
+- **Laravel**: ^10.0|^11.0
+- **Bagisto**: 2.x
+
+---
+
+## 安装步骤
+
+### 1. 确认插件已注册
+
+确保 `bootstrap/providers.php` 文件中已添加:
+
+Longyi\Email\Providers\EmailServiceProvider::class,
+
+### 2. 运行数据库迁移
+composer dump-autoload
+php artisan migrate
+### 3. 初始化默认设置
+php artisan email:init
+### 4. 清理缓存
+php artisan cache:clear      # 清除应用缓存
+php artisan config:clear     # 清除配置缓存
+php artisan view:clear       # 清除视图缓存
+php artisan route:clear      # 清除路由缓存
+
+## 技术支持
+
+如有问题或建议,请联系开发团队。
+
+**开发者**: Longyi Team  
+**邮箱**: dev@longyi.com  
+**许可证**: MIT License

+ 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"
+            ]
+        }
+    }
+}

+ 134 - 0
packages/Longyi/Email/src/Console/Commands/InitializeSettings.php

@@ -0,0 +1,134 @@
+<?php
+
+namespace Longyi\Email\Console\Commands;
+
+use Illuminate\Console\Command;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class InitializeSettings extends Command
+{
+    protected $signature = 'email:init';
+
+    protected $description = 'Initialize email module (create menu items)';
+
+    public function handle()
+    {
+        $this->info('Initializing email module...');
+
+        try {
+            // 添加邮件模块菜单项到动态菜单
+            $this->addMenuItems();
+
+            $this->info('✓ Email module initialized successfully!');
+            $this->info('You can now view email logs in admin panel.');
+
+            return 0;
+
+        } catch (\Exception $e) {
+            $this->error('Error initializing email module: ' . $e->getMessage());
+            $this->error($e->getTraceAsString());
+            return 1;
+        }
+    }
+
+    /**
+     * 添加邮件模块的菜单项到动态菜单
+     */
+    protected function addMenuItems()
+    {
+        $this->info('Adding email menu items to dynamic menu...');
+
+        // 检查是否已存在邮件日志菜单项
+        $existingMenu = MenuItem::where('key', 'email_logs')->first();
+
+        if ($existingMenu) {
+            $this->warn('Email logs menu item already exists in dynamic menu.');
+            return;
+        }
+
+        // 创建邮件日志菜单项
+        $parentItem = MenuItem::create([
+            'name' => '邮件管理',
+            'key' => 'email',
+            'route' => 'admin.configuration.index',
+            'route_parameters' => json_encode(['emails','configure']),
+            'icon' => 'icon-mail',
+            'parent_id' => null,
+            'sort_order' => 90,
+            'status' => 1,
+            'created_by' => 1,
+        ]);
+
+        // 创建子菜单项
+        $childItems = [
+            [
+                'name' => '邮件设置',
+                'key' => 'email.setting',
+                'route' => 'admin.configuration.index',
+                'route_parameters' => json_encode(['emails','configure']),
+                'icon' => 'icon-setting',
+                'sort_order' => 1,
+            ],
+            [
+                'name' => '邮件通知',
+                'key' => 'email.notifications',
+                'route' => 'admin.configuration.index',
+                'route_parameters' => json_encode(['emails','general']),
+                'icon' => 'icon-setting',
+                'sort_order' => 2,
+            ],
+            [
+                'name' => '邮件模版',
+                'key' => 'email.templates',
+                'route' => 'admin.marketing.communications.email_templates.index',
+                'route_parameters' => json_encode([]),
+                'icon' => 'icon-chart',
+                'sort_order' => 3,
+            ],
+            [
+                'name' => '邮件营销',
+                'key' => 'email.events',
+                'route' => 'admin.marketing.communications.events.index',
+                'route_parameters' => json_encode([]),
+                'icon' => 'icon-chart',
+                'sort_order' => 3,
+            ],
+            [
+                'name' => '邮件活动',
+                'key' => 'email.campaigns',
+                'route' => 'admin.marketing.communications.campaigns.index',
+                'route_parameters' => json_encode([]),
+                'icon' => 'icon-chart',
+                'sort_order' => 3,
+            ],
+            [
+                'name' => '邮件日志',
+                'key' => 'email.log',
+                'route' => 'admin.email.logs.index',
+                'route_parameters' => json_encode([]),
+                'icon' => 'icon-setting',
+                'sort_order' => 4,
+            ],
+        ];
+
+        foreach ($childItems as $item) {
+            MenuItem::create([
+                'name' => $item['name'],
+                'key' => $item['key'],
+                'route' => $item['route'],
+                'route_parameters' => $item['route_parameters'],
+                'icon' => $item['icon'],
+                'sort_order' => $item['sort_order'],
+                'parent_id' => $parentItem->id,
+                'status' => 1,
+                'created_by' => 1,
+            ]);
+        }
+
+
+        $this->info('✓ Email logs menu item added to dynamic menu successfully!');
+        $this->info('  - Menu Name: 邮件日志');
+        $this->info('  - Route: admin.email.logs.index');
+        $this->info('  - Icon: fas fa-envelope-open-text');
+    }
+}

+ 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());
+        }
+    }
+}

+ 34 - 0
packages/Longyi/Email/src/Database/Migrations/2026_04_20_000001_create_email_logs_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up()
+    {
+        Schema::create('email_logs', function (Blueprint $table) {
+            $table->id();
+            $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');
+        });
+    }
+
+    public function down()
+    {
+        Schema::dropIfExists('email_logs');
+    }
+};

+ 123 - 0
packages/Longyi/Email/src/Http/Controllers/Admin/LogController.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace Longyi\Email\Http\Controllers\Admin;
+
+use Illuminate\Http\Request;
+use Webkul\Admin\Http\Controllers\Controller;  // 改为继承 Bagisto 的 Admin 控制器
+use Longyi\Email\Models\EmailLog;
+
+class LogController extends Controller
+{
+    protected $_config;
+
+    public function __construct()
+    {
+        $this->_config = request('_config') ?: [];
+    }
+
+    /**
+     * 显示邮件日志列表
+     */
+    public function index(Request $request)
+    {
+        $query = EmailLog::query();
+
+        // 筛选:状态
+        if ($request->filled('status')) {
+            $query->where('status', $request->input('status'));
+        }
+
+        // 筛选:邮箱
+        if ($request->filled('email')) {
+            $query->where('recipient_email', 'like', '%' . $request->input('email') . '%');
+        }
+
+        // 筛选:主题
+        if ($request->filled('subject')) {
+            $query->where('subject', 'like', '%' . $request->input('subject') . '%');
+        }
+
+        // 筛选:日期范围
+        if ($request->filled('date_from')) {
+            $query->whereDate('created_at', '>=', $request->input('date_from'));
+        }
+
+        if ($request->filled('date_to')) {
+            $query->whereDate('created_at', '<=', $request->input('date_to'));
+        }
+
+        $logs = $query->orderBy('created_at', 'desc')->paginate(20);
+
+        // 统计数据
+        $stats = [
+            'total' => EmailLog::count(),
+            'sent' => EmailLog::where('status', 'sent')->count(),
+            'failed' => EmailLog::where('status', 'failed')->count(),
+            'pending' => EmailLog::where('status', 'pending')->count(),
+        ];
+
+        // 使用 _config 中的视图路径
+        $view = isset($this->_config['view']) ? $this->_config['view'] : 'email::admin.logs.index';
+
+        return view($view, compact('logs', 'stats'));
+    }
+
+    /**
+     * 查看邮件详情
+     */
+    public function show($id)
+    {
+        $log = EmailLog::findOrFail($id);
+
+        $view = isset($this->_config['view']) ? $this->_config['view'] : 'email::admin.logs.show';
+
+        return view($view, compact('log'));
+    }
+
+    /**
+     * 删除单条日志
+     */
+    public function destroy($id)
+    {
+        $log = EmailLog::findOrFail($id);
+        $log->delete();
+
+        session()->flash('success', '日志删除成功');
+
+        return redirect()->back();
+    }
+
+    /**
+     * 批量删除日志
+     */
+    public function massDestroy(Request $request)
+    {
+        $ids = $request->input('ids', []);
+
+        if (empty($ids)) {
+            session()->flash('error', '请选择要删除的日志');
+            return redirect()->back();
+        }
+
+        $count = EmailLog::whereIn('id', $ids)->delete();
+
+        session()->flash('success', "成功删除 {$count} 条日志");
+
+        return redirect()->back();
+    }
+
+    /**
+     * 清理旧日志(保留最近30天)
+     */
+    public function cleanOldLogs()
+    {
+        $days = request()->input('days', 30);
+        $date = now()->subDays($days);
+
+        $count = EmailLog::where('created_at', '<', $date)->delete();
+
+        session()->flash('success', "已清理 {$count} 条 {$days} 天前的日志");
+
+        return redirect()->back();
+    }
+}

+ 82 - 0
packages/Longyi/Email/src/Models/EmailLog.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace Longyi\Email\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Builder;
+
+class EmailLog extends Model
+{
+    protected $table = 'email_logs';
+
+    // 禁用自动时间戳,因为表中使用的是 created_at 而不是 timestamps
+    public $timestamps = true;
+
+    protected $fillable = [
+        'recipient_email',
+        'recipient_name',
+        'subject',
+        'content',
+        'template',
+        'status',
+        'type',
+        'metadata',
+        'sent_at',
+        'error_message',
+    ];
+
+    protected $casts = [
+        'metadata' => 'array',
+        'sent_at' => 'datetime',
+    ];
+
+    /**
+     * Scope: 已发送的邮件
+     */
+    public function scopeSuccessful(Builder $query): Builder
+    {
+        return $query->where('status', 'sent');
+    }
+
+    /**
+     * Scope: 失败的邮件
+     */
+    public function scopeFailed(Builder $query): Builder
+    {
+        return $query->where('status', 'failed');
+    }
+
+    /**
+     * Scope: 待发送的邮件
+     */
+    public function scopePending(Builder $query): Builder
+    {
+        return $query->where('status', 'pending');
+    }
+
+    /**
+     * 获取状态文本
+     */
+    public function getStatusTextAttribute(): string
+    {
+        return match($this->status) {
+            'sent' => '已发送',
+            'failed' => '失败',
+            'pending' => '待发送',
+            default => '未知',
+        };
+    }
+
+    /**
+     * 获取状态颜色类
+     */
+    public function getStatusColorClassAttribute(): string
+    {
+        return match($this->status) {
+            'sent' => 'bg-green-100 text-green-600',
+            'failed' => 'bg-red-100 text-red-600',
+            'pending' => 'bg-yellow-100 text-yellow-600',
+            default => 'bg-gray-100 text-gray-600',
+        };
+    }
+}

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

@@ -0,0 +1,45 @@
+<?php
+
+namespace Longyi\Email\Providers;
+
+use Illuminate\Support\ServiceProvider;
+
+class EmailServiceProvider extends ServiceProvider
+{
+    /**
+     * Bootstrap services.
+     */
+    public function boot(): void
+    {
+        // 加载路由
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
+
+        // 加载视图
+        $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'email');
+
+        // 加载语言文件
+        $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'email');
+
+        // 加载迁移
+        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
+
+        // 注册事件监听器
+        $this->app->register(EventServiceProvider::class);
+
+        // 注册命令
+        if ($this->app->runningInConsole()) {
+            $this->commands([
+                \Longyi\Email\Console\Commands\InitializeSettings::class,
+                \Longyi\Email\Console\Commands\TestEmailLogging::class,
+            ]);
+        }
+    }
+
+    /**
+     * Register services.
+     */
+    public function register(): void
+    {
+        //
+    }
+}

+ 934 - 0
packages/Longyi/Email/src/Providers/EventServiceProvider.php

@@ -0,0 +1,934 @@
+<?php
+
+namespace Longyi\Email\Providers;
+
+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 {
+                /*Log::info('JobQueued event triggered', [
+                    'event_class' => get_class($event),
+                ]);*/
+
+                // 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)) {
+                    Log::warning('Payload is not an array', ['payload_type' => gettype($payload)]);
+                    return;
+                }
+
+                if (!isset($payload['data']['command'])) {
+                    Log::info('No command in payload', ['payload_keys' => array_keys($payload)]);
+                    return;
+                }
+
+                $command = @unserialize($payload['data']['command']);
+
+                if (!$command) {
+                    Log::warning('Failed to unserialize command');
+                    return;
+                }
+
+                // 检查是否是 SendQueuedMailable
+                if (!$command instanceof \Illuminate\Mail\SendQueuedMailable) {
+                    Log::info('Not a SendQueuedMailable', [
+                        'command_class' => get_class($command),
+                    ]);
+                    return;
+                }
+
+                $mailable = $command->mailable;
+
+                if (!$mailable instanceof \Illuminate\Mail\Mailable) {
+                    Log::warning('Mailable is not valid');
+                    return;
+                }
+
+                Log::info('Mail job queued', [
+                    'mailable_class' => get_class($mailable),
+                    'queue' => $payload['queue'] ?? 'pending',
+                ]);
+
+                // 关键修复:从 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) {
+                        Log::warning('No email address found for recipient', ['recipient' => $recipient]);
+                        continue;
+                    }
+
+                    // 清理姓名(去除多余空格,如果为空则设为 null)
+                    if ($name) {
+                        $name = trim(preg_replace('/\s+/', ' ', $name));
+                        if (empty($name)) {
+                            $name = null;
+                        }
+                    }
+
+                    // 检查是否已存在(扩大时间窗口到30分钟,避免重复但允许同一用户多次收到不同邮件)
+                    $exists = EmailLog::where('recipient_email', $email)
+                        ->where('subject', $subject)
+                        ->where('created_at', '>', now()->subMinutes(30))
+                        ->exists();
+
+                    if ($exists) {
+                        Log::info('Email log already exists, skipping', [
+                            'email' => $email,
+                            'subject' => $subject,
+                        ]);
+                        continue;
+                    }
+
+                    $log = EmailLog::create([
+                        'recipient_email' => $email,
+                        'recipient_name' => $name,
+                        'subject' => $subject,
+                        'content' => $this->extractContentFromMailable($mailable),
+                        'template' => $this->detectTemplateFromMailable($mailable),
+                        'status' => 'sent',  // 初始状态为 pending(待发送)
+                        'sent_at' => now(),  // 发送时间为空,等待发送成功后更新
+                        'metadata' => json_encode([
+                            'job_id' => $payload['uuid'] ?? null,
+                            'mailable_class' => get_class($mailable),
+                            'queue' => $payload['queue'] ?? null,
+                            'connection' => $payload['connection'] ?? null,
+                            'created_via' => 'JobQueued',
+                        ]),
+                    ]);
+
+                    Log::info('Email log created from queued job', [
+                        'log_id' => $log->id,
+                        'email' => $email,
+                        'name' => $name,
+                        'subject' => $subject,
+                        'status' => 'sent',
+                        'sent_at' => $log->sent_at,
+                    ]);
+                }
+
+            } catch (\Exception $e) {
+                Log::error('Failed to process queued mail job: ' . $e->getMessage(), [
+                    'trace' => $e->getTraceAsString(),
+                ]);
+            }
+        });
+
+    }
+
+
+    /**
+     * 从 SendQueuedMailable 命令中提取收件人
+     */
+    protected function extractRecipientsFromCommand($command, $mailable): array
+    {
+        try {
+            Log::info('Extracting recipients from SendQueuedMailable command', [
+                'command_class' => get_class($command),
+                'mailable_class' => get_class($mailable),
+            ]);
+
+            // 方法1: 从命令对象的 to 属性获取(Laravel 会在构建时填充)
+            if (property_exists($command, 'to') && !empty($command->to)) {
+                Log::info('Found recipients in command->to', ['to' => $command->to]);
+                return is_array($command->to) ? $command->to : [$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 is_array($to) ? $to : [$to];
+                }
+            }
+
+            // 方法3: 从 Mailable 的特定属性推断(customer, subscribersList, order 等)
+            $recipientProperties = ['customer', 'subscribersList', 'order', 'invoice', 'shipment', 'refund'];
+
+            foreach ($recipientProperties as $propName) {
+                if (property_exists($mailable, $propName)) {
+                    $reflection = new \ReflectionClass($mailable);
+                    $property = $reflection->getProperty($propName);
+                    $property->setAccessible(true);
+                    $obj = $property->getValue($mailable);
+
+                    if ($obj && isset($obj->email) && filter_var($obj->email, FILTER_VALIDATE_EMAIL)) {
+                        $recipients = [[
+                            'address' => $obj->email,
+                            'name' => $obj->name ?? $obj->first_name . ' ' . ($obj->last_name ?? '') ?? $obj->customer_full_name ?? null,
+                        ]];
+                        Log::info("Extracted recipient from mailable->{$propName}", [
+                            'recipients' => $recipients,
+                            'object_type' => get_class($obj),
+                        ]);
+                        return $recipients;
+                    }
+                }
+            }
+
+            // 方法4: 检查 toAddresses 属性
+            if (property_exists($mailable, 'toAddresses')) {
+                $toAddresses = $mailable->toAddresses;
+                if (!empty($toAddresses)) {
+                    Log::info('Found toAddresses property', ['toAddresses' => $toAddresses]);
+                    return is_array($toAddresses) ? $toAddresses : [$toAddresses];
+                }
+            }
+
+            // 方法5: 通过反射遍历所有属性,智能识别邮箱
+            $reflection = new \ReflectionClass($mailable);
+            $properties = $reflection->getProperties();
+
+            foreach ($properties as $property) {
+                $property->setAccessible(true);
+                $value = $property->getValue($mailable);
+
+                // 如果属性值是字符串且看起来像邮箱
+                if (is_string($value) && filter_var($value, FILTER_VALIDATE_EMAIL)) {
+                    $recipients = [[
+                        'address' => $value,
+                        'name' => null,
+                    ]];
+                    Log::info("Extracted email from property {$property->getName()}", ['recipients' => $recipients]);
+                    return $recipients;
+                }
+
+                // 如果属性是对象且有 email 字段
+                if (is_object($value) && isset($value->email) && filter_var($value->email, FILTER_VALIDATE_EMAIL)) {
+                    $recipients = [[
+                        'address' => $value->email,
+                        'name' => $value->name ?? $value->first_name . ' ' . ($value->last_name ?? '') ?? null,
+                    ]];
+                    Log::info("Extracted email from object property {$property->getName()}", [
+                        'recipients' => $recipients,
+                        'object_type' => get_class($value),
+                    ]);
+                    return $recipients;
+                }
+            }
+
+            Log::warning('Could not extract recipients from command or mailable', [
+                'command_class' => get_class($command),
+                'mailable_class' => get_class($mailable),
+                'mailable_properties' => array_keys(get_object_vars($mailable)),
+            ]);
+
+            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(),
+            ]);
+        }
+    }
+
+    /**
+     * 从 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)
+                    ->where('status', 'pending')  // 只更新 pending 状态
+                    ->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 update email status: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 处理队列任务
+     */
+    protected function handleQueueJob($job, string $status): 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;
+            }
+
+            $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);
+
+            Log::info('Queue job completed, updating status', [
+                'job_id' => $job->getJobId(),
+                'recipients_count' => count($to),
+                'subject' => $subject,
+            ]);
+
+            foreach ($to as $recipient) {
+                $email = is_array($recipient) ? ($recipient['address'] ?? $recipient['email'] ?? null) : $recipient;
+
+                if (!$email) {
+                    continue;
+                }
+
+                $updated = EmailLog::where('recipient_email', $email)
+                    ->where('subject', $subject)
+                    ->where('status', 'pending')  // 只更新 pending 状态的记录
+                    ->orderBy('created_at', 'desc')
+                    ->limit(1)
+                    ->update([
+                        'status' => 'sent',
+                        'sent_at' => now(),
+                    ]);
+
+                if ($updated) {
+                    Log::info('Email status updated to sent', [
+                        'email' => $email,
+                        'subject' => $subject,
+                    ]);
+                } else {
+                    Log::warning('Failed to update email status', [
+                        'email' => $email,
+                        'subject' => $subject,
+                    ]);
+                }
+            }
+
+        } catch (\Exception $e) {
+            Log::error('Handle queue job after error: ' . $e->getMessage(), [
+                'trace' => $e->getTraceAsString(),
+            ]);
+        }
+    }
+
+
+    /**
+     * 处理队列任务失败
+     */
+    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)
+                    ->where('status', 'pending')  // 只更新 pending 状态的记录
+                    ->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, subscribersList, order 等)
+            $recipientProperties = ['customer', 'subscribersList', 'order', 'invoice', 'shipment', 'refund'];
+
+            foreach ($recipientProperties as $propName) {
+                if ($reflection->hasProperty($propName)) {
+                    $property = $reflection->getProperty($propName);
+                    $property->setAccessible(true);
+                    $obj = $property->getValue($mailable);
+
+                    if ($obj && isset($obj->email)) {
+                        $recipients = [[
+                            'address' => $obj->email,
+                            'name' => $obj->name ?? $obj->first_name ?? $obj->customer_full_name ?? null,
+                        ]];
+                        Log::info("Extracted recipient from mailable->{$propName}", ['recipients' => $recipients]);
+                        return $recipients;
+                    }
+                }
+            }
+
+            // 方法4: 检查 toAddresses 属性
+            if (property_exists($mailable, 'toAddresses')) {
+                $toAddresses = $mailable->toAddresses;
+                if (!empty($toAddresses)) {
+                    Log::info('Found toAddresses property', ['toAddresses' => $toAddresses]);
+                    return $toAddresses;
+                }
+            }
+
+            // 方法5: 通过反射查找所有可能的邮箱字段
+            $properties = $reflection->getProperties();
+
+            foreach ($properties as $property) {
+                $property->setAccessible(true);
+                $value = $property->getValue($mailable);
+
+                // 如果属性值是字符串且看起来像邮箱
+                if (is_string($value) && filter_var($value, FILTER_VALIDATE_EMAIL)) {
+                    $recipients = [[
+                        'address' => $value,
+                        'name' => null,
+                    ]];
+                    Log::info("Extracted email from property {$property->getName()}", ['recipients' => $recipients]);
+                    return $recipients;
+                }
+
+                // 如果属性是对象且有 email 字段
+                if (is_object($value) && isset($value->email) && filter_var($value->email, FILTER_VALIDATE_EMAIL)) {
+                    $recipients = [[
+                        'address' => $value->email,
+                        'name' => $value->name ?? $value->first_name ?? null,
+                    ]];
+                    Log::info("Extracted email from object property {$property->getName()}", ['recipients' => $recipients]);
+                    return $recipients;
+                }
+            }
+
+            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') && !empty($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);
+
+                if (!empty($subject)) {
+                    Log::info('Extracted subject via reflection', ['subject' => $subject]);
+                    return $subject;
+                }
+            }
+
+            // 方法3: 从类名推断并翻译
+            $className = class_basename($mailable);
+            $defaultSubject = str_replace(['Notification', 'Mail'], '', $className);
+
+            // 尝试从语言文件获取翻译
+            $translationKey = 'shop::app.emails.' . strtolower($className) . '.subject';
+            $translatedSubject = trans($translationKey);
+
+            if ($translatedSubject && $translatedSubject !== $translationKey) {
+                Log::info('Using translated subject', ['subject' => $translatedSubject]);
+                return $translatedSubject;
+            }
+
+            // 方法4: 根据邮件类型返回默认主题
+            $subjectMap = [
+                'RegistrationNotification' => trans('shop::app.emails.customers.registration.subject'),
+                'SubscriptionNotification' => trans('shop::app.emails.customers.subscribed.subject'),
+                'EmailVerificationNotification' => trans('shop::app.emails.customers.verification.subject'),
+                'UpdatePasswordNotification' => trans('shop::app.emails.customers.update-password.subject'),
+                'ResetPasswordNotification' => trans('shop::app.emails.customers.reset-password.subject'),
+            ];
+
+            if (isset($subjectMap[$className])) {
+                Log::info('Using mapped subject', ['subject' => $subjectMap[$className]]);
+                return $subjectMap[$className];
+            }
+
+            Log::info('Using default subject from class name', ['subject' => $defaultSubject]);
+            return $defaultSubject;
+        } catch (\Exception $e) {
+            Log::error('Extract subject error: ' . $e->getMessage());
+            return 'Email Notification';
+        }
+    }
+
+
+
+    /**
+     * 从 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 'general';
+    }
+
+    /**
+     * 格式化地址
+     */
+    protected function formatAddresses($addresses): array
+    {
+        if (empty($addresses)) {
+            return [];
+        }
+
+        $result = [];
+        foreach ($addresses as $email => $name) {
+            $result[] = [
+                'email' => $email,
+                'name' => is_string($name) ? $name : null,
+            ];
+        }
+
+        return $result;
+    }
+}

+ 49 - 0
packages/Longyi/Email/src/Resources/lang/zh_CN/app.php

@@ -0,0 +1,49 @@
+<?php
+
+return [
+    'email' => [
+        'title' => '邮件管理',
+        'logs' => '邮件日志',
+
+        // 日志列表
+        'recipient_email' => '收件人邮箱',
+        'recipient_name' => '收件人姓名',
+        'subject' => '主题',
+        'status' => '状态',
+        'sent_at' => '发送时间',
+        'created_at' => '创建时间',
+        'view_details' => '查看详情',
+        'delete' => '删除',
+        'mass_delete' => '批量删除',
+        'clean_old_logs' => '清理旧日志',
+
+        // 状态
+        'status_sent' => '已发送',
+        'status_failed' => '发送失败',
+        'status_pending' => '待发送',
+
+        // 筛选
+        'filter_by_status' => '按状态筛选',
+        'filter_by_email' => '按邮箱筛选',
+        'filter_by_subject' => '按主题筛选',
+        'filter_by_date' => '按日期筛选',
+        'all_status' => '全部状态',
+
+        // 统计
+        'total_logs' => '总日志数',
+        'sent_count' => '已发送',
+        'failed_count' => '发送失败',
+        'pending_count' => '待发送',
+
+        // 详情
+        'email_content' => '邮件内容',
+        'error_message' => '错误信息',
+        'metadata' => '元数据',
+        'template_used' => '使用模板',
+
+        // 清理
+        'clean_old_logs_confirm' => '确定要清理旧日志吗?',
+        'keep_days' => '保留天数',
+        'clean' => '清理',
+    ],
+];

+ 225 - 0
packages/Longyi/Email/src/Resources/views/admin/logs/index.blade.php

@@ -0,0 +1,225 @@
+<x-admin::layouts>
+    <x-slot:title>
+        邮件日志管理
+    </x-slot:title>
+
+    <div class="flex items-center justify-between gap-4 max-sm:flex-wrap">
+        <p class="text-xl font-bold text-gray-800 dark:text-white">
+            邮件发送日志
+        </p>
+
+        <div class="flex items-center gap-x-2.5">
+            <!-- 清理旧日志按钮
+            @if(route('admin.email.logs.clean_old'))
+                <form action="{{ route('admin.email.logs.clean_old') }}" method="POST" onsubmit="return confirm('确定要清理30天前的旧日志吗?')">
+                    @csrf
+                    <input type="hidden" name="days" value="30">
+                    <button type="submit" class="secondary-button">
+                        🗑️ 清理30天前日志
+                    </button>
+                </form>
+            @endif
+            -->
+        </div>
+    </div>
+
+    <!-- 统计卡片
+    <div class="mt-4 grid grid-cols-4 gap-4">
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <p class="text-sm text-gray-500">总邮件数</p>
+            <p class="text-2xl font-bold">{{ number_format($stats['total'] ?? 0) }}</p>
+        </div>
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <p class="text-sm text-green-600">发送成功</p>
+            <p class="text-2xl font-bold text-green-600">{{ number_format($stats['sent'] ?? 0) }}</p>
+        </div>
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <p class="text-sm text-red-600">发送失败</p>
+            <p class="text-2xl font-bold text-red-600">{{ number_format($stats['failed'] ?? 0) }}</p>
+        </div>
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <p class="text-sm text-yellow-600">等待发送</p>
+            <p class="text-2xl font-bold text-yellow-600">{{ number_format($stats['pending'] ?? 0) }}</p>
+        </div>
+    </div>
+    -->
+    <!-- 筛选表单 -->
+    <div class="mt-4 rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+        <form method="GET" action="{{ route('admin.email.logs.index') }}" class="grid grid-cols-4 gap-4">
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-1">
+                    状态筛选
+                </label>
+                <select name="status" class="control">
+                    <option value="">全部状态</option>
+                    <option value="sent" {{ request('status') == 'sent' ? 'selected' : '' }}>✓ 成功</option>
+                    <option value="failed" {{ request('status') == 'failed' ? 'selected' : '' }}>✗ 失败</option>
+                    <option value="pending" {{ request('status') == 'pending' ? 'selected' : '' }}>⏳ 等待</option>
+                </select>
+            </div>
+
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-1">
+                    邮箱地址
+                </label>
+                <input type="text" name="email" value="{{ request('email') }}" placeholder="输入邮箱地址" class="control">
+            </div>
+
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-1">
+                    开始日期
+                </label>
+                <input type="date" name="date_from" value="{{ request('date_from') }}" class="control">
+            </div>
+
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-1">
+                    结束日期
+                </label>
+                <input type="date" name="date_to" value="{{ request('date_to') }}" class="control">
+            </div>
+
+            <div class="flex items-end">
+                <button type="submit" class="primary-button w-full">
+                    🔍 筛选查询
+                </button>
+            </div>
+        </form>
+    </div>
+
+    <!-- 批量操作栏
+    <div class="mt-4 flex justify-between items-center">
+        <div>
+            <button type="button" onclick="massDelete()" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700">
+                🗑️ 批量删除
+            </button>
+        </div>
+    </div>
+-->
+    <!-- 日志列表 -->
+    <div class="mt-4 rounded-lg bg-white shadow dark:bg-gray-900">
+        @if(isset($logs) && $logs->isNotEmpty())
+            <form id="massDeleteForm" method="POST" action="{{ route('admin.email.logs.mass_delete') }}">
+                @csrf
+                @method('DELETE')
+
+                <div class="overflow-x-auto">
+                    <table class="w-full">
+                        <thead>
+                        <tr class="border-b dark:border-gray-800">
+                            <th class="px-4 py-3 text-left w-10">
+                                <input type="checkbox" id="selectAll" class="cursor-pointer">
+                            </th>
+                            <th class="px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300">
+                                收件人
+                            </th>
+                            <th class="px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300">
+                                邮件主题
+                            </th>
+                            <th class="px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300">
+                                状态
+                            </th>
+                            <th class="px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300">
+                                发送时间
+                            </th>
+                            <th class="px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300">
+                                操作
+                            </th>
+                        </tr>
+                        </thead>
+                        <tbody>
+                        @foreach($logs as $log)
+                            <tr class="border-b dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800">
+                                <td class="px-4 py-3">
+                                    <input type="checkbox" name="ids[]" value="{{ $log->id }}" class="cursor-pointer checkbox-item">
+                                </td>
+                                <td class="px-4 py-3 text-sm text-gray-800 dark:text-white">
+                                    <strong>{{ $log->recipient_email }}</strong>
+                                    @if($log->recipient_name)
+                                        <br><small class="text-gray-500">{{ $log->recipient_name }}</small>
+                                    @endif
+                                </td>
+                                <td class="px-4 py-3 text-sm text-gray-800 dark:text-white max-w-md truncate" title="{{ $log->subject ?? 'N/A' }}">
+                                    {{ $log->subject ?? '无主题' }}
+                                </td>
+                                <td class="px-4 py-3">
+                                    @if($log->status === 'sent')
+                                        <span class="rounded bg-green-100 px-2 py-1 text-xs text-green-600">
+                                            ✓ 成功
+                                        </span>
+                                    @elseif($log->status === 'failed')
+                                        <span class="rounded bg-red-100 px-2 py-1 text-xs text-red-600">
+                                            ✗ 失败
+                                        </span>
+                                    @else
+                                        <span class="rounded bg-yellow-100 px-2 py-1 text-xs text-yellow-600">
+                                            ⏳ 等待
+                                        </span>
+                                    @endif
+                                </td>
+                                <td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">
+                                    {{ $log->created_at ? $log->created_at->format('Y-m-d H:i:s') : 'N/A' }}
+                                </td>
+                                <td class="px-4 py-3">
+                                    <div class="flex gap-2">
+                                        <a href="{{ route('admin.email.logs.show', $log->id) }}"
+                                           class="cursor-pointer text-blue-600 hover:underline">
+                                            查看详情
+                                        </a>
+                                        <form action="{{ route('admin.email.logs.delete', $log->id) }}"
+                                              method="POST"
+                                              onsubmit="return confirm('确定删除这条日志?')"
+                                              style="display:inline;">
+                                            @csrf
+                                            @method('DELETE')
+                                            <button type="submit" class="text-red-600 hover:underline">
+                                                删除
+                                            </button>
+                                        </form>
+                                    </div>
+                                </td>
+                            </tr>
+                        @endforeach
+                        </tbody>
+                    </table>
+                </div>
+
+                @if($logs->hasPages())
+                    <div class="border-t px-4 py-3 dark:border-gray-800">
+                        {{ $logs->links() }}
+                    </div>
+                @endif
+            </form>
+        @else
+            <div class="px-4 py-8 text-center text-gray-500">
+                📭 暂无邮件日志记录
+            </div>
+        @endif
+    </div>
+
+    @push('scripts')
+        <script>
+            // 全选功能
+            const selectAll = document.getElementById('selectAll');
+            if (selectAll) {
+                selectAll.addEventListener('change', function() {
+                    const checkboxes = document.querySelectorAll('.checkbox-item');
+                    checkboxes.forEach(cb => cb.checked = this.checked);
+                });
+            }
+
+            // 批量删除
+            function massDelete() {
+                const checkboxes = document.querySelectorAll('.checkbox-item:checked');
+                if (checkboxes.length === 0) {
+                    alert('请选择要删除的日志');
+                    return;
+                }
+
+                if (confirm(`确定要删除 ${checkboxes.length} 条日志吗?`)) {
+                    document.getElementById('massDeleteForm').submit();
+                }
+            }
+        </script>
+    @endpush
+</x-admin::layouts>

+ 85 - 0
packages/Longyi/Email/src/Resources/views/admin/logs/show.blade.php

@@ -0,0 +1,85 @@
+<x-admin::layouts>
+    <x-slot:title>
+        邮件详情
+    </x-slot:title>
+
+    <div class="flex items-center justify-between">
+        <p class="text-xl font-bold text-gray-800 dark:text-white">
+            邮件详情
+        </p>
+        <a href="{{ route('admin.email.logs.index') }}" class="secondary-button">
+            ← 返回列表
+        </a>
+    </div>
+
+    <div class="mt-4 space-y-4">
+        <!-- 基本信息 -->
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <h3 class="mb-3 text-lg font-semibold">基本信息</h3>
+            <div class="grid grid-cols-2 gap-4">
+                <div>
+                    <label class="text-sm text-gray-500">收件人邮箱</label>
+                    <p class="font-medium">{{ $log->recipient_email }}</p>
+                </div>
+                <div>
+                    <label class="text-sm text-gray-500">收件人姓名</label>
+                    <p class="font-medium">{{ $log->recipient_name ?? '-' }}</p>
+                </div>
+                <div>
+                    <label class="text-sm text-gray-500">发送状态</label>
+                    <p>
+                        @if($log->status === 'sent')
+                            <span class="rounded bg-green-100 px-2 py-1 text-xs text-green-600">✓ 成功</span>
+                        @elseif($log->status === 'failed')
+                            <span class="rounded bg-red-100 px-2 py-1 text-xs text-red-600">✗ 失败</span>
+                        @else
+                            <span class="rounded bg-yellow-100 px-2 py-1 text-xs text-yellow-600">⏳ 等待</span>
+                        @endif
+                    </p>
+                </div>
+                <div>
+                    <label class="text-sm text-gray-500">使用模板</label>
+                    <p class="font-medium">{{ $log->template ?? '-' }}</p>
+                </div>
+                <div>
+                    <label class="text-sm text-gray-500">创建时间</label>
+                    <p class="font-medium">{{ $log->created_at->format('Y-m-d H:i:s') }}</p>
+                </div>
+                <div>
+                    <label class="text-sm text-gray-500">发送时间</label>
+                    <p class="font-medium">{{ $log->sent_at?->format('Y-m-d H:i:s') ?? '-' }}</p>
+                </div>
+            </div>
+        </div>
+
+        <!-- 邮件主题 -->
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <h3 class="mb-3 text-lg font-semibold">邮件主题</h3>
+            <p class="text-gray-800 dark:text-white">{{ $log->subject }}</p>
+        </div>
+
+        <!-- 错误信息 -->
+        @if($log->error_message)
+            <div class="rounded-lg bg-red-50 p-4 shadow dark:bg-red-900/20">
+                <h3 class="mb-3 text-lg font-semibold text-red-600">错误信息</h3>
+                <pre class="whitespace-pre-wrap text-sm text-red-600">{{ $log->error_message }}</pre>
+            </div>
+        @endif
+
+        <!-- 邮件内容 -->
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <h3 class="mb-3 text-lg font-semibold">邮件内容</h3>
+            <div class="prose max-w-none dark:prose-invert">
+                {!! $log->content !!}
+            </div>
+        </div>
+
+        <!-- 元数据 -->
+        @if($log->metadata)
+            <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+                <h3 class="mb-3 text-lg font-semibold">元数据信息</h3>
+                <pre class="overflow-x-auto rounded bg-gray-100 p-3 text-sm dark:bg-gray-800">{{ json_encode($log->metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
+            </div>
+        @endif
+    </div>
+</x-admin::layouts>

+ 33 - 0
packages/Longyi/Email/src/Routes/admin-routes.php

@@ -0,0 +1,33 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\Email\Http\Controllers\Admin\LogController;
+
+// 使用正确的路由组,包含 admin 中间件和 admin 前缀
+Route::group(['middleware' => ['web', 'admin'], 'prefix' => 'admin'], function () {
+
+    Route::controller(LogController::class)->prefix('email/logs')->group(function () {
+        // 邮件日志列表
+        Route::get('', 'index')
+            ->defaults('_config', [
+                'view' => 'email::admin.logs.index'
+            ])
+            ->name('admin.email.logs.index');
+
+        // 查看邮件详情
+        Route::get('show/{id}', 'show')
+            ->defaults('_config', [
+                'view' => 'email::admin.logs.show'
+            ])
+            ->name('admin.email.logs.show');
+
+        // 删除单条日志
+        Route::delete('delete/{id}', 'destroy')->name('admin.email.logs.delete');
+
+        // 批量删除
+        Route::post('mass-delete', 'massDestroy')->name('admin.email.logs.mass_delete');
+
+        // 清理旧日志
+        Route::post('clean-old', 'cleanOldLogs')->name('admin.email.logs.clean_old');
+    });
+});

+ 0 - 1
packages/Longyi/RewardPoints/src/Http/Controllers/RewardPointsController.php

@@ -142,7 +142,6 @@ class RewardPointsController extends Controller
         if (!$customer) {
             return ApiResponse::unauthorized();
         }
-        Log::info('Customer login event triggered');
         $today = Carbon::now()->format('Y-m-d');
         $signedToday = RewardPointCustomerSign::where('customer_id', $customer->id)
             ->where('sign_date', $today)

+ 7 - 2
packages/Longyi/RewardPoints/src/Listeners/OrderEvents.php

@@ -9,9 +9,11 @@ use Longyi\RewardPoints\Services\GrowthValueService;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 use Webkul\Sales\Models\Order;
+use Longyi\RewardPoints\Services\LevelCalculationTrait;
 
 class OrderEvents
 {
+    use LevelCalculationTrait;
     protected RewardPointRepository $rewardPointRepository;
     protected GrowthValueService $growthValueService;
 
@@ -25,6 +27,7 @@ class OrderEvents
 
     public function handleOrderPlacement(Order $order): void
     {
+        $startTime = microtime(true);
         if (!$order->customer_id) {
             return;
         }
@@ -67,12 +70,14 @@ class OrderEvents
 
             Log::info('Order points added as PENDING', [
                 'order_id' => $order->id,
+                'customer_id' => $order->customer_id,
                 'points' => $points,
-                'rate' => $pointsPerCurrency
+                'rate' => $pointsPerCurrency,
+                'order_amount' => $order->base_grand_total,
+                'execution_time' => $this->getExecutionTime($startTime)
             ]);
         }
     }
-
     public function handleOrderCancellation(Order $order): void
     {
         if (!$order->customer_id) {

+ 18 - 0
packages/Longyi/RewardPoints/src/Services/LevelCalculationTrait.php

@@ -54,4 +54,22 @@ trait LevelCalculationTrait
             return $setting ? $setting->value : $default;
         });
     }
+    /**
+     * 计算执行时间(毫秒)
+     *
+     * @param float $startTime 开始时间戳(microtime(true))
+     * @param int $precision 小数位数,默认2位
+     * @return string 格式化的执行时间,如 "12.35ms"
+     *
+     * @example
+     * $start = microtime(true);
+     * // ... 执行某些操作 ...
+     * echo $this->getExecutionTime($start); // 输出: "15.23ms"
+     */
+    protected function getExecutionTime(float $startTime, int $precision = 2): string
+    {
+        $endTime = microtime(true);
+        $executionTime = ($endTime - $startTime) * 1000; // 转换为毫秒
+        return number_format($executionTime, $precision) . 'ms';
+    }
 }

+ 88 - 8
packages/Webkul/Core/src/Menu.php

@@ -91,8 +91,39 @@ class Menu
         $menuWithDotNotation = [];
 
         foreach ($this->configMenu as $item) {
-            if (strpos(request()->url(), route($item['route'])) !== false) {
-                $this->currentKey = $item['key'];
+            // 验证菜单项必须有必要的字段
+            if (!isset($item['key']) || !isset($item['name']) || !isset($item['route'])) {
+                continue;
+            }
+
+            // 安全获取路由参数
+            $routeParameters = $item['route_parameters'] ?? [];
+
+            // 检查是否有路由参数
+            if (!empty($routeParameters)) {
+                try {
+                    $url = route($item['route'], $routeParameters);
+                    if (strpos(request()->url(), $url) !== false) {
+                        $this->currentKey = $item['key'];
+                    }
+                } catch (\Exception $e) {
+                    // 如果路由生成失败,回退到原来的逻辑
+                    try {
+                        if (strpos(request()->url(), route($item['route'])) !== false) {
+                            $this->currentKey = $item['key'];
+                        }
+                    } catch (\Exception $e2) {
+                        // 忽略错误
+                    }
+                }
+            } else {
+                try {
+                    if (strpos(request()->url(), route($item['route'])) !== false) {
+                        $this->currentKey = $item['key'];
+                    }
+                } catch (\Exception $e) {
+                    // 忽略错误
+                }
             }
 
             $menuWithDotNotation[$item['key']] = $item;
@@ -101,19 +132,29 @@ class Menu
         $menu = Arr::undot(Arr::dot($menuWithDotNotation));
 
         foreach ($menu as $menuItemKey => $menuItem) {
+            // 验证顶级菜单项
+            if (!is_array($menuItem) || !isset($menuItem['key'])) {
+                continue;
+            }
+
             $subMenuItems = $this->processSubMenuItems($menuItem);
 
+            // 安全解析路由参数
+            $routeParameters = $this->parseRouteParameters($menuItem['route_parameters'] ?? []);
+
             $this->addItem(new MenuItem(
                 key: $menuItemKey,
                 name: trans($menuItem['name']),
                 route: $menuItem['route'],
-                sort: $menuItem['sort'],
-                icon: $menuItem['icon'],
+                sort: $menuItem['sort'] ?? 0,
+                icon: $menuItem['icon'] ?? '',
                 children: $subMenuItems,
+                routeParameters: $routeParameters,
             ));
         }
     }
 
+
     /**
      * Process sub menu items.
      */
@@ -121,21 +162,60 @@ class Menu
     {
         return collect($menuItem)
             ->sortBy('sort')
-            ->filter(fn ($value) => is_array($value))
+            ->filter(fn ($value, $key) => is_array($value) && isset($value['key']))
             ->map(function ($subMenuItem) {
+                // 确保子菜单项有必要的字段
+                if (!isset($subMenuItem['key']) || !isset($subMenuItem['name']) || !isset($subMenuItem['route'])) {
+                    return null;
+                }
+
                 $subSubMenuItems = $this->processSubMenuItems($subMenuItem);
 
+                // 安全解析子菜单的路由参数
+                $routeParameters = $this->parseRouteParameters($subMenuItem['route_parameters'] ?? []);
+
                 return new MenuItem(
                     key: $subMenuItem['key'],
                     name: trans($subMenuItem['name']),
                     route: $subMenuItem['route'],
-                    sort: $subMenuItem['sort'],
-                    icon: $subMenuItem['icon'],
+                    sort: $subMenuItem['sort'] ?? 0,
+                    icon: $subMenuItem['icon'] ?? '',
                     children: $subSubMenuItems,
+                    routeParameters: $routeParameters,
                 );
-            });
+            })
+            ->filter(); // 过滤掉 null 值
     }
 
+
+    /**
+     * Parse route parameters from various formats.
+     *
+     * @param mixed $parameters
+     * @return array
+     */
+    private function parseRouteParameters($parameters): array
+    {
+        if (empty($parameters)) {
+            return [];
+        }
+
+        // 如果已经是数组,直接返回
+        if (is_array($parameters)) {
+            // 过滤空值
+            return array_values(array_filter($parameters));
+        }
+
+        // 如果是 JSON 字符串,解码
+        if (is_string($parameters)) {
+            $decoded = json_decode($parameters, true);
+            if (is_array($decoded)) {
+                return array_values(array_filter($decoded));
+            }
+        }
+
+        return [];
+    }
     /**
      * Get current active menu.
      */

+ 14 - 1
packages/Webkul/Core/src/Menu/MenuItem.php

@@ -18,6 +18,7 @@ class MenuItem
         public int $sort,
         public string $icon,
         public Collection $children,
+        public array $routeParameters = [],
     ) {}
 
     /**
@@ -44,12 +45,24 @@ class MenuItem
         return $this->route;
     }
 
+    /**
+     * Get route parameters.
+     */
+    public function getRouteParameters(): array
+    {
+        return $this->routeParameters;
+    }
+
     /**
      * Get the url of the menu item.
      */
     public function getUrl(): string
     {
-        return route($this->getRoute());
+        if (empty($this->routeParameters)) {
+            return route($this->getRoute());
+        }
+
+        return route($this->getRoute(), $this->routeParameters);
     }
 
     /**