1
0

90 کامیت‌ها 2c1f66cd8d ... d61fbc14b4

نویسنده SHA1 پیام تاریخ
  chengwl d61fbc14b4 Merge branch 'variant' of http://gogs.hnwmzp.cn/chengwenliang/nshop into dev 1 ماه پیش
  bianjunhui 64587af53f Merge branch 'dev-rewardPoints' into dev 1 ماه پیش
  bianjunhui 7d376bb340 S3图片空值 1 ماه پیش
  bianjunhui 346a256a11 Merge branch 'dev-rewardPoints' into dev 1 ماه پیش
  bianjunhui dfe138267a 数组 1 ماه پیش
  bianjunhui 513b873a66 Merge branch 'dev-rewardPoints' into dev 1 ماه پیش
  bianjunhui 3a965cae23 接口整合 1 ماه پیش
  bianjunhui 2058bc1e5f Merge branch 'dev-rewardPoints' into dev 1 ماه پیش
  bianjunhui 2e9f3e70c7 关注 1 ماه پیش
  bianjunhui db13714fbb 浏览产品 1 ماه پیش
  llp 0d894532c7 添加支付方式 1 ماه پیش
  bianjunhui 185e36cd90 Merge branch 'dev-rewardPoints' into dev 1 ماه پیش
  bianjunhui 05594684ed 积分 1 ماه پیش
  bianjunhui bf604f76d3 Merge branch 'dev-rewardPoints' into dev 1 ماه پیش
  bianjunhui e3ef9d7e59 单个地址 1 ماه پیش
  bianjunhui 98524744a7 Merge branch 'dev-rewardPoints' into dev 1 ماه پیش
  bianjunhui fd40b92f1a rest 地址 1 ماه پیش
  llp 3a71b47550 修改支付方式 1 ماه پیش
  llp 27e5ac219d 调整支付方式 1 ماه پیش
  llp 3778f9e997 修改支付方式 1 ماه پیش
  llp 385ab8c82a 修改支付方式 1 ماه پیش
  llp 63b1cae77c Merge branch 'llp_pay_26_06_01' into dev 1 ماه پیش
  llp 0a7d676842 修改支付方式 1 ماه پیش
  chengwl d678024e86 Merge branch 'dev' of http://gogs.hnwmzp.cn/chengwenliang/nshop into dev 1 ماه پیش
  chengwl 0df4b58e0a Merge branch 'variant' of http://gogs.hnwmzp.cn/chengwenliang/nshop into dev 1 ماه پیش
  llp 1635ced9f7 修改礼品卡里列表 1 ماه پیش
  llp 2efde004da 修改礼品卡列表 1 ماه پیش
  llp bf42e39333 修改礼品卡列表接口 1 ماه پیش
  llp aaf6d1f25a 修改礼品卡列表接口 1 ماه پیش
  llp ccc72d7a88 修改字段 1 ماه پیش
  bianjunhui e8ba8f5874 Merge branch 'dev-rewardPoints' into dev 2 ماه پیش
  bianjunhui a611a3a213 修复 2 ماه پیش
  llp df735b2f4b 修改兑换文案 2 ماه پیش
  llp 8a0ea5d12a Merge branch 'dev' of http://gogs.hnwmzp.cn/chengwenliang/nshop into dev 2 ماه پیش
  llp b3b90075e9 修改积分兑换接口 2 ماه پیش
  bianjunhui a8148bdec8 Merge branch 'dev-rewardPoints' into dev 2 ماه پیش
  bianjunhui d657da4019 Merge branch 'dev' into dev-rewardPoints 2 ماه پیش
  bianjunhui 5a3767b664 X-STOREFRONT-KEY 验证 2 ماه پیش
  llp 6c2ebbe61c 修改礼品卡列表 2 ماه پیش
  llp 03689a6ca4 修改返回 2 ماه پیش
  llp c848d412eb 修改添加礼品卡接口 2 ماه پیش
  llp 70d893645b 修改添加积分异常 2 ماه پیش
  llp 1ceaa3287a Merge branch 'llp_gift_26_04_10' into dev 2 ماه پیش
  llp bbf8a18568 修改礼品卡添加 2 ماه پیش
  llp 4ba87fc580 Merge branch 'llp_gift_26_04_10' into dev 2 ماه پیش
  llp c13ca439bd 添加礼品卡路由 2 ماه پیش
  llp fc8bf091c3 图片库 2 ماه پیش
  bianjunhui abe6d88d8e FileAdder 2 ماه پیش
  llp cae4125db7 Merge branch 'llp_gift_26_04_10' into dev 2 ماه پیش
  llp 86c3ee5523 修改礼品卡 2 ماه پیش
  bianjunhui 584bea821e Merge branch 'dev-rewardPoints' into dev 2 ماه پیش
  bianjunhui 835a970911 图片上传 2 ماه پیش
  bianjunhui 6ecce24dda Merge branch 'dev' into dev-rewardPoints 2 ماه پیش
  bianjunhui 98da143b11 Undefined array key "resolver" 2 ماه پیش
  chengwl 23760f1418 1 2 ماه پیش
  chengwl cd1ff1e86e 1 2 ماه پیش
  llp b21c2d03a5 添加礼品卡迁移脚本 2 ماه پیش
  llp a1279719fb 修改礼品卡接口 2 ماه پیش
  llp 84a3705323 添加个人中心礼品卡接口 2 ماه پیش
  bianjunhui dfc844111d Merge branch 'dev-rewardPoints' into dev 2 ماه پیش
  bianjunhui 0553f88f0e 图片上传到S3 2 ماه پیش
  bianjunhui fc45ff1b34 产品图片上传到S3 2 ماه پیش
  bianjunhui bc5c00e02f 评论图片上传S3 2 ماه پیش
  llp 85feadfecb 支付方式 2 ماه پیش
  bianjunhui d5b0a5e3e7 图片上传 2 ماه پیش
  bianjunhui e5551baa20 优化 2 ماه پیش
  bianjunhui 64e0c1a0e6 积分修复 2 ماه پیش
  bianjunhui 4a991b5bba 积分修复 2 ماه پیش
  chengwl 5019b0ec1a Merge branch 'variant' of http://gogs.hnwmzp.cn/chengwenliang/nshop into dev 2 ماه پیش
  bianjunhui c8023d7eba Merge branch 'dev-rewardPoints' into dev 3 ماه پیش
  bianjunhui 4e7dc2b3d8 分页 3 ماه پیش
  bianjunhui c52aa281d5 Merge branch 'dev-rewardPoints' into dev 3 ماه پیش
  bianjunhui 85210dde76 json 3 ماه پیش
  bianjunhui 8a0eb4575c Merge branch 'dev-rewardPoints' into dev 3 ماه پیش
  bianjunhui f759e56566 邮件日志 3 ماه پیش
  bianjunhui 7f7daed44f 邮件日志 3 ماه پیش
  llp 3334667ad4 Merge branch 'llp_member_26_04_16' into dev 3 ماه پیش
  bianjunhui 43c0e529ab 邮件日志 3 ماه پیش
  bianjunhui 9fc06d3b83 自定义菜单更改 3 ماه پیش
  llp 9959536b19 添加字段 3 ماه پیش
  llp d5ce8ea175 添加是否是vip的属性 3 ماه پیش
  llp 45483b23a1 添加会员功能 3 ماه پیش
  llp 3b01903e58 Merge branch 'dev' into llp_member_26_04_16 3 ماه پیش
  llp de41b42a0d 添加用户会员 3 ماه پیش
  bianjunhui cf598e276b 自定义菜单路由验证 3 ماه پیش
  llp c9a715a3c9 Merge branch 'dev' into llp_member_26_04_16 3 ماه پیش
  llp 51db6bf26d 添加字段 3 ماه پیش
  bianjunhui 3a26e323a6 优化 3 ماه پیش
  llp 9ff4beafc8 Merge branch 'dev' into llp_member_26_04_16 3 ماه پیش
  llp 57a5861124 vip功能 3 ماه پیش
100فایلهای تغییر یافته به همراه8633 افزوده شده و 278 حذف شده
  1. 0 2
      bootstrap/cache/.gitignore
  2. 210 0
      bootstrap/cache/packages.php
  3. 410 0
      bootstrap/cache/services.php
  4. 8 0
      bootstrap/providers.php
  5. 9 0
      composer.json
  6. 268 1
      composer.lock
  7. 239 0
      config/media-library.php
  8. 19 0
      config/visitor.php
  9. 16 12
      packages/Longyi/Core/src/Database/Migrations/2026_04_16_000001_alter_product_variant_images_use_media_id.php
  10. 20 3
      packages/Longyi/Core/src/Repositories/ProductImageRepository.php
  11. 29 0
      packages/Longyi/DynamicMenu/src/Database/Migrations/add_route_parameters_to_menu_items.php
  12. 148 43
      packages/Longyi/DynamicMenu/src/Http/Controllers/MenuController.php
  13. 7 6
      packages/Longyi/DynamicMenu/src/Models/MenuItem.php
  14. 38 0
      packages/Longyi/DynamicMenu/src/Providers/DynamicMenuServiceProvider.php
  15. 227 79
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/create.blade.php
  16. 275 37
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/edit.blade.php
  17. 16 12
      packages/Longyi/DynamicMenu/src/Routes/admin-routes.php
  18. 34 0
      packages/Longyi/Email/README.md
  19. 26 0
      packages/Longyi/Email/composer.json
  20. 134 0
      packages/Longyi/Email/src/Console/Commands/InitializeSettings.php
  21. 68 0
      packages/Longyi/Email/src/Console/Commands/TestEmailLogging.php
  22. 34 0
      packages/Longyi/Email/src/Database/Migrations/2026_04_20_000001_create_email_logs_table.php
  23. 123 0
      packages/Longyi/Email/src/Http/Controllers/Admin/LogController.php
  24. 82 0
      packages/Longyi/Email/src/Models/EmailLog.php
  25. 45 0
      packages/Longyi/Email/src/Providers/EmailServiceProvider.php
  26. 934 0
      packages/Longyi/Email/src/Providers/EventServiceProvider.php
  27. 49 0
      packages/Longyi/Email/src/Resources/lang/zh_CN/app.php
  28. 225 0
      packages/Longyi/Email/src/Resources/views/admin/logs/index.blade.php
  29. 85 0
      packages/Longyi/Email/src/Resources/views/admin/logs/show.blade.php
  30. 33 0
      packages/Longyi/Email/src/Routes/admin-routes.php
  31. 111 0
      packages/Longyi/Gift/src/Console/Commands/ImportGiftCards.php
  32. 65 59
      packages/Longyi/Gift/src/Http/Controllers/Shop/GiftController.php
  33. 8 8
      packages/Longyi/Gift/src/Listeners/InvoiceHandler.php
  34. 132 1
      packages/Longyi/Gift/src/Models/GiftCards.php
  35. 1 1
      packages/Longyi/Gift/src/Providers/EventServiceProvider.php
  36. 12 3
      packages/Longyi/Gift/src/Repositories/CustomInvoiceRepository.php
  37. 10 2
      packages/Longyi/Gift/src/Repositories/CustomOrderRepository.php
  38. 21 0
      packages/Longyi/Gift/src/Resources/views/sales/invoices/view.blade.php
  39. 4 1
      packages/Longyi/Gift/src/Resources/views/sales/orders/view.blade.php
  40. 11 8
      packages/Longyi/Gift/src/Routes/shop-routes.php
  41. 633 0
      packages/Longyi/ImageUpload/FRONTEND_EXAMPLES.md
  42. 191 0
      packages/Longyi/ImageUpload/INSTALLATION.md
  43. 152 0
      packages/Longyi/ImageUpload/QUICKSTART.md
  44. 507 0
      packages/Longyi/ImageUpload/README.md
  45. 31 0
      packages/Longyi/ImageUpload/composer.json
  46. 65 0
      packages/Longyi/ImageUpload/src/Config/image_upload.php
  47. 166 0
      packages/Longyi/ImageUpload/src/Http/Controllers/Admin/ImageUploadController.php
  48. 169 0
      packages/Longyi/ImageUpload/src/Http/Controllers/Shop/ImageUploadController.php
  49. 62 0
      packages/Longyi/ImageUpload/src/Providers/ImageUploadServiceProvider.php
  50. 25 0
      packages/Longyi/ImageUpload/src/Resources/lang/en/app.php
  51. 25 0
      packages/Longyi/ImageUpload/src/Resources/lang/zh_CN/app.php
  52. 291 0
      packages/Longyi/ImageUpload/src/Resources/views/admin/image-upload/index.blade.php
  53. 520 0
      packages/Longyi/ImageUpload/src/Resources/views/components/image-upload.blade.php
  54. 13 0
      packages/Longyi/ImageUpload/src/Routes/admin-routes.php
  55. 20 0
      packages/Longyi/ImageUpload/src/Routes/shop-routes.php
  56. 200 0
      packages/Longyi/ImageUpload/src/Services/ImageUploadService.php
  57. 23 0
      packages/Longyi/Member/package.json
  58. 6 0
      packages/Longyi/Member/postcss.config.js
  59. 10 0
      packages/Longyi/Member/src/Config/acl.php
  60. 11 0
      packages/Longyi/Member/src/Config/admin-menu.php
  61. 7 0
      packages/Longyi/Member/src/Contracts/MemberLog.php
  62. 107 0
      packages/Longyi/Member/src/DataGrids/Member/MemberDataGrid.php
  63. 28 0
      packages/Longyi/Member/src/Database/Migrations/2026_04_15_233520_add_vip_expire_fields_to_customer_table.php
  64. 31 0
      packages/Longyi/Member/src/Database/Migrations/2026_04_15_233955_add_member_fields_to_cart_table.php
  65. 31 0
      packages/Longyi/Member/src/Database/Migrations/2026_04_15_234016_add_member_fields_to_orders_table.php
  66. 31 0
      packages/Longyi/Member/src/Database/Migrations/2026_04_15_234035_add_member_fields_to_invoices_table.php
  67. 32 0
      packages/Longyi/Member/src/Database/Migrations/2026_04_20_180518_create_member_log_table.php
  68. 23 0
      packages/Longyi/Member/src/Http/Controllers/Admin/MemberController.php
  69. 17 0
      packages/Longyi/Member/src/Http/Controllers/Shop/MemberController.php
  70. 99 0
      packages/Longyi/Member/src/Http/Controllers/Shop/MemberDiscountController.php
  71. 47 0
      packages/Longyi/Member/src/Listeners/MemberHandler.php
  72. 63 0
      packages/Longyi/Member/src/Listeners/OrderPlacedHandler.php
  73. 28 0
      packages/Longyi/Member/src/Listeners/OrderViewHandler.php
  74. 98 0
      packages/Longyi/Member/src/Listeners/VipDiscountHandler.php
  75. 39 0
      packages/Longyi/Member/src/Models/MemberLog.php
  76. 9 0
      packages/Longyi/Member/src/Models/MemberLogProxy.php
  77. 33 0
      packages/Longyi/Member/src/Providers/EventServiceProvider.php
  78. 55 0
      packages/Longyi/Member/src/Providers/MemberServiceProvider.php
  79. 15 0
      packages/Longyi/Member/src/Providers/ModuleServiceProvider.php
  80. 16 0
      packages/Longyi/Member/src/Repositories/MemberLogRepository.php
  81. 23 0
      packages/Longyi/Member/src/Resources/assets/css/app.css
  82. 12 0
      packages/Longyi/Member/src/Resources/assets/images/icon-temp-active.svg
  83. 12 0
      packages/Longyi/Member/src/Resources/assets/images/icon-temp.svg
  84. 4 0
      packages/Longyi/Member/src/Resources/assets/js/app.js
  85. 9 0
      packages/Longyi/Member/src/Resources/lang/en/app.php
  86. 9 0
      packages/Longyi/Member/src/Resources/lang/zh_CN/app.php
  87. 19 0
      packages/Longyi/Member/src/Resources/views/admin/index.blade.php
  88. 1 0
      packages/Longyi/Member/src/Resources/views/admin/layouts/style.blade.php
  89. 16 0
      packages/Longyi/Member/src/Resources/views/components/member-discount-cartsummary.blade.php
  90. 183 0
      packages/Longyi/Member/src/Resources/views/components/vip-member-discount-cartsummary.blade.php
  91. 22 0
      packages/Longyi/Member/src/Resources/views/customers/account/orders/member-info.blade.php
  92. 29 0
      packages/Longyi/Member/src/Resources/views/sales/orders/view.blade.php
  93. 11 0
      packages/Longyi/Member/src/Resources/views/shop/index.blade.php
  94. 10 0
      packages/Longyi/Member/src/Routes/admin-routes.php
  95. 14 0
      packages/Longyi/Member/src/Routes/shop-routes.php
  96. 4 0
      packages/Longyi/Member/tailwind.config.js
  97. 46 0
      packages/Longyi/Member/vite.config.js
  98. 12 0
      packages/Longyi/Pay/Afterpay/src/Config/payment-methods.php
  99. 82 0
      packages/Longyi/Pay/Afterpay/src/Config/system.php
  100. 0 0
      packages/Longyi/Pay/Afterpay/src/Http/Controllers/AfterpayController.php

+ 0 - 2
bootstrap/cache/.gitignore

@@ -1,2 +0,0 @@
-*
-!.gitignore

+ 210 - 0
bootstrap/cache/packages.php

@@ -0,0 +1,210 @@
+<?php return array (
+  'astrotomic/laravel-translatable' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Astrotomic\\Translatable\\TranslatableServiceProvider',
+    ),
+  ),
+  'bagisto/bagisto-package-generator' => 
+  array (
+    'aliases' => 
+    array (
+    ),
+    'providers' => 
+    array (
+      0 => 'Webkul\\PackageGenerator\\Providers\\PackageGeneratorServiceProvider',
+    ),
+  ),
+  'bagisto/laravel-datafaker' => 
+  array (
+    'aliases' => 
+    array (
+    ),
+    'providers' => 
+    array (
+      0 => 'Webkul\\Faker\\Providers\\FakerServiceProvider',
+    ),
+  ),
+  'barryvdh/laravel-debugbar' => 
+  array (
+    'aliases' => 
+    array (
+      'Debugbar' => 'Barryvdh\\Debugbar\\Facades\\Debugbar',
+    ),
+    'providers' => 
+    array (
+      0 => 'Barryvdh\\Debugbar\\ServiceProvider',
+    ),
+  ),
+  'barryvdh/laravel-dompdf' => 
+  array (
+    'aliases' => 
+    array (
+      'PDF' => 'Barryvdh\\DomPDF\\Facade\\Pdf',
+      'Pdf' => 'Barryvdh\\DomPDF\\Facade\\Pdf',
+    ),
+    'providers' => 
+    array (
+      0 => 'Barryvdh\\DomPDF\\ServiceProvider',
+    ),
+  ),
+  'diglactic/laravel-breadcrumbs' => 
+  array (
+    'aliases' => 
+    array (
+      'Breadcrumbs' => 'Diglactic\\Breadcrumbs\\Breadcrumbs',
+    ),
+    'providers' => 
+    array (
+      0 => 'Diglactic\\Breadcrumbs\\ServiceProvider',
+    ),
+  ),
+  'kalnoy/nestedset' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Kalnoy\\Nestedset\\NestedSetServiceProvider',
+    ),
+  ),
+  'konekt/concord' => 
+  array (
+    'aliases' => 
+    array (
+      'Helper' => 'Konekt\\Concord\\Facades\\Helper',
+      'Concord' => 'Konekt\\Concord\\Facades\\Concord',
+    ),
+    'providers' => 
+    array (
+      0 => 'Konekt\\Concord\\ConcordServiceProvider',
+    ),
+  ),
+  'konekt/enum-eloquent' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Konekt\\Enum\\Eloquent\\EnumServiceProvider',
+    ),
+  ),
+  'laravel/octane' => 
+  array (
+    'aliases' => 
+    array (
+      'Octane' => 'Laravel\\Octane\\Facades\\Octane',
+    ),
+    'providers' => 
+    array (
+      0 => 'Laravel\\Octane\\OctaneServiceProvider',
+    ),
+  ),
+  'laravel/sanctum' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Laravel\\Sanctum\\SanctumServiceProvider',
+    ),
+  ),
+  'laravel/tinker' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Laravel\\Tinker\\TinkerServiceProvider',
+    ),
+  ),
+  'laravel/ui' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Laravel\\Ui\\UiServiceProvider',
+    ),
+  ),
+  'maatwebsite/excel' => 
+  array (
+    'aliases' => 
+    array (
+      'Excel' => 'Maatwebsite\\Excel\\Facades\\Excel',
+    ),
+    'providers' => 
+    array (
+      0 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
+    ),
+  ),
+  'nesbot/carbon' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Carbon\\Laravel\\ServiceProvider',
+    ),
+  ),
+  'nunomaduro/collision' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
+    ),
+  ),
+  'nunomaduro/termwind' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Termwind\\Laravel\\TermwindServiceProvider',
+    ),
+  ),
+  'openai-php/laravel' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'OpenAI\\Laravel\\ServiceProvider',
+    ),
+  ),
+  'pestphp/pest-plugin-laravel' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Pest\\Laravel\\PestServiceProvider',
+    ),
+  ),
+  'prettus/l5-repository' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Prettus\\Repository\\Providers\\RepositoryServiceProvider',
+    ),
+  ),
+  'spatie/laravel-medialibrary' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Spatie\\MediaLibrary\\MediaLibraryServiceProvider',
+    ),
+  ),
+  'spatie/laravel-responsecache' => 
+  array (
+    'aliases' => 
+    array (
+      'ResponseCache' => 'Spatie\\ResponseCache\\Facades\\ResponseCache',
+    ),
+    'providers' => 
+    array (
+      0 => 'Spatie\\ResponseCache\\ResponseCacheServiceProvider',
+    ),
+  ),
+  'spatie/laravel-sitemap' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Spatie\\Sitemap\\SitemapServiceProvider',
+    ),
+  ),
+  'stevebauman/purify' => 
+  array (
+    'aliases' => 
+    array (
+      'Purify' => 'Stevebauman\\Purify\\Facades\\Purify',
+    ),
+    'providers' => 
+    array (
+      0 => 'Stevebauman\\Purify\\PurifyServiceProvider',
+    ),
+  ),
+);

+ 410 - 0
bootstrap/cache/services.php

@@ -0,0 +1,410 @@
+<?php return array (
+  'providers' => 
+  array (
+    0 => 'Illuminate\\Auth\\AuthServiceProvider',
+    1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
+    2 => 'Illuminate\\Bus\\BusServiceProvider',
+    3 => 'Illuminate\\Cache\\CacheServiceProvider',
+    4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    5 => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
+    6 => 'Illuminate\\Cookie\\CookieServiceProvider',
+    7 => 'Illuminate\\Database\\DatabaseServiceProvider',
+    8 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
+    9 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
+    10 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
+    11 => 'Illuminate\\Hashing\\HashServiceProvider',
+    12 => 'Illuminate\\Mail\\MailServiceProvider',
+    13 => 'Illuminate\\Notifications\\NotificationServiceProvider',
+    14 => 'Illuminate\\Pagination\\PaginationServiceProvider',
+    15 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
+    16 => 'Illuminate\\Pipeline\\PipelineServiceProvider',
+    17 => 'Illuminate\\Queue\\QueueServiceProvider',
+    18 => 'Illuminate\\Redis\\RedisServiceProvider',
+    19 => 'Illuminate\\Session\\SessionServiceProvider',
+    20 => 'Illuminate\\Translation\\TranslationServiceProvider',
+    21 => 'Illuminate\\Validation\\ValidationServiceProvider',
+    22 => 'Illuminate\\View\\ViewServiceProvider',
+    23 => 'Astrotomic\\Translatable\\TranslatableServiceProvider',
+    24 => 'Webkul\\PackageGenerator\\Providers\\PackageGeneratorServiceProvider',
+    25 => 'Webkul\\Faker\\Providers\\FakerServiceProvider',
+    26 => 'Barryvdh\\Debugbar\\ServiceProvider',
+    27 => 'Barryvdh\\DomPDF\\ServiceProvider',
+    28 => 'Diglactic\\Breadcrumbs\\ServiceProvider',
+    29 => 'Kalnoy\\Nestedset\\NestedSetServiceProvider',
+    30 => 'Konekt\\Concord\\ConcordServiceProvider',
+    31 => 'Konekt\\Enum\\Eloquent\\EnumServiceProvider',
+    32 => 'Laravel\\Octane\\OctaneServiceProvider',
+    33 => 'Laravel\\Sanctum\\SanctumServiceProvider',
+    34 => 'Laravel\\Tinker\\TinkerServiceProvider',
+    35 => 'Laravel\\Ui\\UiServiceProvider',
+    36 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
+    37 => 'Carbon\\Laravel\\ServiceProvider',
+    38 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
+    39 => 'Termwind\\Laravel\\TermwindServiceProvider',
+    40 => 'OpenAI\\Laravel\\ServiceProvider',
+    41 => 'Pest\\Laravel\\PestServiceProvider',
+    42 => 'Prettus\\Repository\\Providers\\RepositoryServiceProvider',
+    43 => 'Spatie\\MediaLibrary\\MediaLibraryServiceProvider',
+    44 => 'Spatie\\ResponseCache\\ResponseCacheServiceProvider',
+    45 => 'Spatie\\Sitemap\\SitemapServiceProvider',
+    46 => 'Stevebauman\\Purify\\PurifyServiceProvider',
+    47 => 'ApiPlatform\\Laravel\\ApiPlatformProvider',
+    48 => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    49 => 'ApiPlatform\\Laravel\\Eloquent\\ApiPlatformEventProvider',
+    50 => 'App\\Providers\\AppServiceProvider',
+    51 => 'Webkul\\Admin\\Providers\\AdminServiceProvider',
+    52 => 'Longyi\\Core\\Providers\\LongyiCoreServiceProvider',
+    53 => 'Longyi\\DynamicMenu\\Providers\\DynamicMenuServiceProvider',
+    54 => 'Longyi\\Email\\Providers\\EmailServiceProvider',
+    55 => 'Longyi\\ImageUpload\\Providers\\ImageUploadServiceProvider',
+    56 => 'Longyi\\RewardPoints\\Providers\\RewardPointsServiceProvider',
+    57 => 'Longyi\\Member\\Providers\\MemberServiceProvider',
+    58 => 'Longyi\\Gift\\Providers\\GiftServiceProvider',
+    59 => 'Longyi\\Pay\\AwxAfterpay\\Providers\\AwxAfterpayServiceProvider',
+    60 => 'Longyi\\Pay\\AwxKlarna\\Providers\\AwxKlarnaServiceProvider',
+    61 => 'Longyi\\Pay\\Applepay\\Providers\\ApplepayServiceProvider',
+    62 => 'Longyi\\Pay\\Airwallex\\Providers\\AirwallexServiceProvider',
+    63 => 'Longyi\\Pay\\Afterpay\\Providers\\AfterpayServiceProvider',
+    64 => 'Webkul\\Attribute\\Providers\\AttributeServiceProvider',
+    65 => 'Webkul\\BookingProduct\\Providers\\BookingProductServiceProvider',
+    66 => 'Webkul\\CMS\\Providers\\CMSServiceProvider',
+    67 => 'Webkul\\CartRule\\Providers\\CartRuleServiceProvider',
+    68 => 'Webkul\\CatalogRule\\Providers\\CatalogRuleServiceProvider',
+    69 => 'Webkul\\Category\\Providers\\CategoryServiceProvider',
+    70 => 'Webkul\\Checkout\\Providers\\CheckoutServiceProvider',
+    71 => 'Webkul\\Core\\Providers\\CoreServiceProvider',
+    72 => 'Webkul\\Core\\Providers\\EnvValidatorServiceProvider',
+    73 => 'Webkul\\Customer\\Providers\\CustomerServiceProvider',
+    74 => 'Webkul\\DataGrid\\Providers\\DataGridServiceProvider',
+    75 => 'Webkul\\DataTransfer\\Providers\\DataTransferServiceProvider',
+    76 => 'Webkul\\DebugBar\\Providers\\DebugBarServiceProvider',
+    77 => 'Webkul\\FPC\\Providers\\FPCServiceProvider',
+    78 => 'Webkul\\GDPR\\Providers\\GDPRServiceProvider',
+    79 => 'Webkul\\Installer\\Providers\\InstallerServiceProvider',
+    80 => 'Webkul\\Inventory\\Providers\\InventoryServiceProvider',
+    81 => 'Webkul\\MagicAI\\Providers\\MagicAIServiceProvider',
+    82 => 'Webkul\\Marketing\\Providers\\MarketingServiceProvider',
+    83 => 'Webkul\\Notification\\Providers\\NotificationServiceProvider',
+    84 => 'Webkul\\Payment\\Providers\\PaymentServiceProvider',
+    85 => 'Webkul\\Paypal\\Providers\\PaypalServiceProvider',
+    86 => 'Webkul\\Product\\Providers\\ProductServiceProvider',
+    87 => 'Webkul\\Rule\\Providers\\RuleServiceProvider',
+    88 => 'Webkul\\Sales\\Providers\\SalesServiceProvider',
+    89 => 'Webkul\\Shipping\\Providers\\ShippingServiceProvider',
+    90 => 'Webkul\\Shop\\Providers\\ShopServiceProvider',
+    91 => 'Webkul\\Sitemap\\Providers\\SitemapServiceProvider',
+    92 => 'Webkul\\SocialLogin\\Providers\\SocialLoginServiceProvider',
+    93 => 'Webkul\\SocialShare\\Providers\\SocialShareServiceProvider',
+    94 => 'Webkul\\Tax\\Providers\\TaxServiceProvider',
+    95 => 'Webkul\\Theme\\Providers\\ThemeServiceProvider',
+    96 => 'Webkul\\User\\Providers\\UserServiceProvider',
+    97 => 'Webkul\\BagistoApi\\Providers\\BagistoApiServiceProvider',
+  ),
+  'eager' => 
+  array (
+    0 => 'Illuminate\\Auth\\AuthServiceProvider',
+    1 => 'Illuminate\\Cookie\\CookieServiceProvider',
+    2 => 'Illuminate\\Database\\DatabaseServiceProvider',
+    3 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
+    4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
+    5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
+    6 => 'Illuminate\\Notifications\\NotificationServiceProvider',
+    7 => 'Illuminate\\Pagination\\PaginationServiceProvider',
+    8 => 'Illuminate\\Session\\SessionServiceProvider',
+    9 => 'Illuminate\\View\\ViewServiceProvider',
+    10 => 'Astrotomic\\Translatable\\TranslatableServiceProvider',
+    11 => 'Webkul\\PackageGenerator\\Providers\\PackageGeneratorServiceProvider',
+    12 => 'Webkul\\Faker\\Providers\\FakerServiceProvider',
+    13 => 'Barryvdh\\Debugbar\\ServiceProvider',
+    14 => 'Barryvdh\\DomPDF\\ServiceProvider',
+    15 => 'Kalnoy\\Nestedset\\NestedSetServiceProvider',
+    16 => 'Konekt\\Concord\\ConcordServiceProvider',
+    17 => 'Konekt\\Enum\\Eloquent\\EnumServiceProvider',
+    18 => 'Laravel\\Octane\\OctaneServiceProvider',
+    19 => 'Laravel\\Sanctum\\SanctumServiceProvider',
+    20 => 'Laravel\\Ui\\UiServiceProvider',
+    21 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
+    22 => 'Carbon\\Laravel\\ServiceProvider',
+    23 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
+    24 => 'Termwind\\Laravel\\TermwindServiceProvider',
+    25 => 'Pest\\Laravel\\PestServiceProvider',
+    26 => 'Prettus\\Repository\\Providers\\RepositoryServiceProvider',
+    27 => 'Spatie\\MediaLibrary\\MediaLibraryServiceProvider',
+    28 => 'Spatie\\ResponseCache\\ResponseCacheServiceProvider',
+    29 => 'Spatie\\Sitemap\\SitemapServiceProvider',
+    30 => 'Stevebauman\\Purify\\PurifyServiceProvider',
+    31 => 'ApiPlatform\\Laravel\\ApiPlatformProvider',
+    32 => 'ApiPlatform\\Laravel\\Eloquent\\ApiPlatformEventProvider',
+    33 => 'App\\Providers\\AppServiceProvider',
+    34 => 'Webkul\\Admin\\Providers\\AdminServiceProvider',
+    35 => 'Longyi\\Core\\Providers\\LongyiCoreServiceProvider',
+    36 => 'Longyi\\DynamicMenu\\Providers\\DynamicMenuServiceProvider',
+    37 => 'Longyi\\Email\\Providers\\EmailServiceProvider',
+    38 => 'Longyi\\ImageUpload\\Providers\\ImageUploadServiceProvider',
+    39 => 'Longyi\\RewardPoints\\Providers\\RewardPointsServiceProvider',
+    40 => 'Longyi\\Member\\Providers\\MemberServiceProvider',
+    41 => 'Longyi\\Gift\\Providers\\GiftServiceProvider',
+    42 => 'Longyi\\Pay\\AwxAfterpay\\Providers\\AwxAfterpayServiceProvider',
+    43 => 'Longyi\\Pay\\AwxKlarna\\Providers\\AwxKlarnaServiceProvider',
+    44 => 'Longyi\\Pay\\Applepay\\Providers\\ApplepayServiceProvider',
+    45 => 'Longyi\\Pay\\Airwallex\\Providers\\AirwallexServiceProvider',
+    46 => 'Longyi\\Pay\\Afterpay\\Providers\\AfterpayServiceProvider',
+    47 => 'Webkul\\Attribute\\Providers\\AttributeServiceProvider',
+    48 => 'Webkul\\BookingProduct\\Providers\\BookingProductServiceProvider',
+    49 => 'Webkul\\CMS\\Providers\\CMSServiceProvider',
+    50 => 'Webkul\\CartRule\\Providers\\CartRuleServiceProvider',
+    51 => 'Webkul\\CatalogRule\\Providers\\CatalogRuleServiceProvider',
+    52 => 'Webkul\\Category\\Providers\\CategoryServiceProvider',
+    53 => 'Webkul\\Checkout\\Providers\\CheckoutServiceProvider',
+    54 => 'Webkul\\Core\\Providers\\CoreServiceProvider',
+    55 => 'Webkul\\Core\\Providers\\EnvValidatorServiceProvider',
+    56 => 'Webkul\\Customer\\Providers\\CustomerServiceProvider',
+    57 => 'Webkul\\DataGrid\\Providers\\DataGridServiceProvider',
+    58 => 'Webkul\\DataTransfer\\Providers\\DataTransferServiceProvider',
+    59 => 'Webkul\\DebugBar\\Providers\\DebugBarServiceProvider',
+    60 => 'Webkul\\FPC\\Providers\\FPCServiceProvider',
+    61 => 'Webkul\\GDPR\\Providers\\GDPRServiceProvider',
+    62 => 'Webkul\\Installer\\Providers\\InstallerServiceProvider',
+    63 => 'Webkul\\Inventory\\Providers\\InventoryServiceProvider',
+    64 => 'Webkul\\MagicAI\\Providers\\MagicAIServiceProvider',
+    65 => 'Webkul\\Marketing\\Providers\\MarketingServiceProvider',
+    66 => 'Webkul\\Notification\\Providers\\NotificationServiceProvider',
+    67 => 'Webkul\\Payment\\Providers\\PaymentServiceProvider',
+    68 => 'Webkul\\Paypal\\Providers\\PaypalServiceProvider',
+    69 => 'Webkul\\Product\\Providers\\ProductServiceProvider',
+    70 => 'Webkul\\Rule\\Providers\\RuleServiceProvider',
+    71 => 'Webkul\\Sales\\Providers\\SalesServiceProvider',
+    72 => 'Webkul\\Shipping\\Providers\\ShippingServiceProvider',
+    73 => 'Webkul\\Shop\\Providers\\ShopServiceProvider',
+    74 => 'Webkul\\Sitemap\\Providers\\SitemapServiceProvider',
+    75 => 'Webkul\\SocialLogin\\Providers\\SocialLoginServiceProvider',
+    76 => 'Webkul\\SocialShare\\Providers\\SocialShareServiceProvider',
+    77 => 'Webkul\\Tax\\Providers\\TaxServiceProvider',
+    78 => 'Webkul\\Theme\\Providers\\ThemeServiceProvider',
+    79 => 'Webkul\\User\\Providers\\UserServiceProvider',
+    80 => 'Webkul\\BagistoApi\\Providers\\BagistoApiServiceProvider',
+  ),
+  'deferred' => 
+  array (
+    'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
+    'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
+    'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
+    'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
+    'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
+    'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
+    'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
+    'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
+    'cache' => 'Illuminate\\Cache\\CacheServiceProvider',
+    'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider',
+    'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider',
+    'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider',
+    'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider',
+    'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Concurrency\\Console\\InvokeSerializedClosureCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ApiInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\BroadcastingInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ClassMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ConfigPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\EnumMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\InterfaceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\JobMiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\TraitMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
+    'Illuminate\\Concurrency\\ConcurrencyManager' => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
+    'hash' => 'Illuminate\\Hashing\\HashServiceProvider',
+    'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider',
+    'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider',
+    'mailer' => 'Illuminate\\Mail\\MailServiceProvider',
+    'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider',
+    'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
+    'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
+    'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
+    'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
+    'queue' => 'Illuminate\\Queue\\QueueServiceProvider',
+    'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider',
+    'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider',
+    'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider',
+    'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider',
+    'redis' => 'Illuminate\\Redis\\RedisServiceProvider',
+    'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider',
+    'translator' => 'Illuminate\\Translation\\TranslationServiceProvider',
+    'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider',
+    'validator' => 'Illuminate\\Validation\\ValidationServiceProvider',
+    'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider',
+    'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider',
+    'Diglactic\\Breadcrumbs\\Manager' => 'Diglactic\\Breadcrumbs\\ServiceProvider',
+    'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
+    'OpenAI\\Client' => 'OpenAI\\Laravel\\ServiceProvider',
+    'OpenAI\\Contracts\\ClientContract' => 'OpenAI\\Laravel\\ServiceProvider',
+    'openai' => 'OpenAI\\Laravel\\ServiceProvider',
+    'ApiPlatform\\State\\CallableProvider' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'ApiPlatform\\State\\CallableProcessor' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'ApiPlatform\\Laravel\\Eloquent\\State\\ItemProvider' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'ApiPlatform\\Laravel\\Eloquent\\State\\CollectionProvider' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'ApiPlatform\\Serializer\\Parameter\\SerializerFilterParameterProvider' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'ApiPlatform\\State\\Provider\\ParameterProvider' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'ApiPlatform\\Laravel\\Eloquent\\Extension\\FilterQueryExtension' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'filters' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'ApiPlatform\\Metadata\\Resource\\Factory\\ResourceMetadataCollectionFactoryInterface' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'api_platform.graphql.state_provider.parameter' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'ApiPlatform\\GraphQl\\Type\\FieldsBuilderEnumInterface' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    'ApiPlatform\\Laravel\\ExceptionHandlerInterface' => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+  ),
+  'when' => 
+  array (
+    'Illuminate\\Broadcasting\\BroadcastServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Bus\\BusServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Cache\\CacheServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Concurrency\\ConcurrencyServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Hashing\\HashServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Mail\\MailServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Pipeline\\PipelineServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Queue\\QueueServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Redis\\RedisServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Translation\\TranslationServiceProvider' => 
+    array (
+    ),
+    'Illuminate\\Validation\\ValidationServiceProvider' => 
+    array (
+    ),
+    'Diglactic\\Breadcrumbs\\ServiceProvider' => 
+    array (
+    ),
+    'Laravel\\Tinker\\TinkerServiceProvider' => 
+    array (
+    ),
+    'OpenAI\\Laravel\\ServiceProvider' => 
+    array (
+    ),
+    'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider' => 
+    array (
+    ),
+  ),
+);

+ 8 - 0
bootstrap/providers.php

@@ -12,8 +12,16 @@ return [
     Webkul\Admin\Providers\AdminServiceProvider::class,
     Longyi\Core\Providers\LongyiCoreServiceProvider::class,
     Longyi\DynamicMenu\Providers\DynamicMenuServiceProvider::class,
+    Longyi\Email\Providers\EmailServiceProvider::class,
+    Longyi\ImageUpload\Providers\ImageUploadServiceProvider::class,
     Longyi\RewardPoints\Providers\RewardPointsServiceProvider::class,
+    Longyi\Member\Providers\MemberServiceProvider::class,
     Longyi\Gift\Providers\GiftServiceProvider::class,
+    Longyi\Pay\AwxAfterpay\Providers\AwxAfterpayServiceProvider::class,
+    Longyi\Pay\AwxKlarna\Providers\AwxKlarnaServiceProvider::class,
+    Longyi\Pay\Applepay\Providers\ApplepayServiceProvider::class,
+    Longyi\Pay\Airwallex\Providers\AirwallexServiceProvider::class,
+    Longyi\Pay\Afterpay\Providers\AfterpayServiceProvider::class,
     Webkul\Attribute\Providers\AttributeServiceProvider::class,
     Webkul\BookingProduct\Providers\BookingProductServiceProvider::class,
     Webkul\CMS\Providers\CMSServiceProvider::class,

+ 9 - 0
composer.json

@@ -37,6 +37,7 @@
         "laravel/socialite": "^5.0",
         "laravel/tinker": "^2.0",
         "laravel/ui": "^4.0",
+        "league/flysystem-aws-s3-v3": "^3.0",
         "maatwebsite/excel": "^3.1.46",
         "mpdf/mpdf": "^8.2",
         "nesbot/carbon": "^2.72.2",
@@ -69,8 +70,16 @@
             "Database\\Seeders\\": "database/seeders/",
             "Longyi\\Core\\": "packages/Longyi/Core/src/",
             "Longyi\\DynamicMenu\\": "packages/Longyi/DynamicMenu/src/",
+            "Longyi\\Email\\": "packages/Longyi/Email/src/",
+            "Longyi\\ImageUpload\\": "packages/Longyi/ImageUpload/src/",
             "Longyi\\RewardPoints\\": "packages/Longyi/RewardPoints/src/",
+            "Longyi\\Member\\": "packages/Longyi/Member/src/",
             "Longyi\\Gift\\": "packages/Longyi/Gift/src/",
+            "Longyi\\Pay\\Airwallex\\": "packages/Longyi/Pay/Airwallex/src/",
+            "Longyi\\Pay\\Applepay\\": "packages/Longyi/Pay/Applepay/src/",
+            "Longyi\\Pay\\AwxKlarna\\": "packages/Longyi/Pay/AwxKlarna/src/",
+            "Longyi\\Pay\\AwxAfterpay\\": "packages/Longyi/Pay/AwxAfterpay/src/",
+            "Longyi\\Pay\\Afterpay\\": "packages/Longyi/Pay/Afterpay/src/",
             "Webkul\\Admin\\": "packages/Webkul/Admin/src",
             "Webkul\\Attribute\\": "packages/Webkul/Attribute/src",
             "Webkul\\BookingProduct\\": "packages/Webkul/BookingProduct/src",

+ 268 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "d3db3887e839723d5366412b0ebe8f01",
+    "content-hash": "c378778fb9d9f7b784858297dea09780",
     "packages": [
         {
             "name": "api-platform/documentation",
@@ -1160,6 +1160,157 @@
             ],
             "time": "2024-08-28T09:20:26+00:00"
         },
+        {
+            "name": "aws/aws-crt-php",
+            "version": "v1.2.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/awslabs/aws-crt-php.git",
+                "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e",
+                "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
+                "yoast/phpunit-polyfills": "^1.0"
+            },
+            "suggest": {
+                "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "AWS SDK Common Runtime Team",
+                    "email": "aws-sdk-common-runtime@amazon.com"
+                }
+            ],
+            "description": "AWS Common Runtime for PHP",
+            "homepage": "https://github.com/awslabs/aws-crt-php",
+            "keywords": [
+                "amazon",
+                "aws",
+                "crt",
+                "sdk"
+            ],
+            "support": {
+                "issues": "https://github.com/awslabs/aws-crt-php/issues",
+                "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7"
+            },
+            "time": "2024-10-18T22:15:13+00:00"
+        },
+        {
+            "name": "aws/aws-sdk-php",
+            "version": "3.384.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/aws/aws-sdk-php.git",
+                "reference": "a6c85b3a1f5f8327076291040329214b3a2caf2b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a6c85b3a1f5f8327076291040329214b3a2caf2b",
+                "reference": "a6c85b3a1f5f8327076291040329214b3a2caf2b",
+                "shasum": ""
+            },
+            "require": {
+                "aws/aws-crt-php": "^1.2.3",
+                "ext-json": "*",
+                "ext-pcre": "*",
+                "ext-simplexml": "*",
+                "guzzlehttp/guzzle": "^7.4.5",
+                "guzzlehttp/promises": "^2.0",
+                "guzzlehttp/psr7": "^2.4.5",
+                "mtdowling/jmespath.php": "^2.8.0",
+                "php": ">=8.1",
+                "psr/http-message": "^1.0 || ^2.0",
+                "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0"
+            },
+            "require-dev": {
+                "andrewsville/php-token-reflection": "^1.4",
+                "aws/aws-php-sns-message-validator": "~1.0",
+                "behat/behat": "~3.0",
+                "composer/composer": "^2.7.8",
+                "dms/phpunit-arraysubset-asserts": "^v0.5.0",
+                "doctrine/cache": "~1.4",
+                "ext-dom": "*",
+                "ext-openssl": "*",
+                "ext-sockets": "*",
+                "phpunit/phpunit": "^10.0",
+                "psr/cache": "^2.0 || ^3.0",
+                "psr/simple-cache": "^2.0 || ^3.0",
+                "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0",
+                "yoast/phpunit-polyfills": "^2.0"
+            },
+            "suggest": {
+                "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
+                "doctrine/cache": "To use the DoctrineCacheAdapter",
+                "ext-curl": "To send requests using cURL",
+                "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
+                "ext-pcntl": "To use client-side monitoring",
+                "ext-sockets": "To use client-side monitoring"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions.php"
+                ],
+                "psr-4": {
+                    "Aws\\": "src/"
+                },
+                "exclude-from-classmap": [
+                    "src/data/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Amazon Web Services",
+                    "homepage": "https://aws.amazon.com"
+                }
+            ],
+            "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
+            "homepage": "https://aws.amazon.com/sdk-for-php",
+            "keywords": [
+                "amazon",
+                "aws",
+                "cloud",
+                "dynamodb",
+                "ec2",
+                "glacier",
+                "s3",
+                "sdk"
+            ],
+            "support": {
+                "forum": "https://github.com/aws/aws-sdk-php/discussions",
+                "issues": "https://github.com/aws/aws-sdk-php/issues",
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.384.2"
+            },
+            "time": "2026-06-03T18:07:33+00:00"
+        },
         {
             "name": "bagisto/bagisto-package-generator",
             "version": "v2.1.2",
@@ -4591,6 +4742,61 @@
             },
             "time": "2024-10-08T08:58:34+00:00"
         },
+        {
+            "name": "league/flysystem-aws-s3-v3",
+            "version": "3.34.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
+                "reference": "0c62fdac907791d8649ad3c61cb7a77628344fb8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/0c62fdac907791d8649ad3c61cb7a77628344fb8",
+                "reference": "0c62fdac907791d8649ad3c61cb7a77628344fb8",
+                "shasum": ""
+            },
+            "require": {
+                "aws/aws-sdk-php": "^3.371.5",
+                "league/flysystem": "^3.10.0",
+                "league/mime-type-detection": "^1.0.0",
+                "php": "^8.0.2"
+            },
+            "conflict": {
+                "guzzlehttp/guzzle": "<7.0",
+                "guzzlehttp/ringphp": "<1.1.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "League\\Flysystem\\AwsS3V3\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Frank de Jonge",
+                    "email": "info@frankdejonge.nl"
+                }
+            ],
+            "description": "AWS S3 filesystem adapter for Flysystem.",
+            "keywords": [
+                "Flysystem",
+                "aws",
+                "file",
+                "files",
+                "filesystem",
+                "s3",
+                "storage"
+            ],
+            "support": {
+                "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.34.0"
+            },
+            "time": "2026-05-04T08:24:00+00:00"
+        },
         {
             "name": "league/flysystem-local",
             "version": "3.29.0",
@@ -5620,6 +5826,67 @@
             },
             "time": "2023-05-03T06:19:36+00:00"
         },
+        {
+            "name": "mtdowling/jmespath.php",
+            "version": "2.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/jmespath/jmespath.php.git",
+                "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://repo.huaweicloud.com/repository/php/mtdowling/jmespath.php/2.8.0/mtdowling-jmespath.php-2.8.0.zip",
+                "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0",
+                "symfony/polyfill-mbstring": "^1.17"
+            },
+            "require-dev": {
+                "composer/xdebug-handler": "^3.0.3",
+                "phpunit/phpunit": "^8.5.33"
+            },
+            "bin": [
+                "bin/jp.php"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.8-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/JmesPath.php"
+                ],
+                "psr-4": {
+                    "JmesPath\\": "src/"
+                }
+            },
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Declaratively specify how to extract elements from a JSON document",
+            "keywords": [
+                "json",
+                "jsonpath"
+            ],
+            "time": "2024-09-04T18:46:31+00:00"
+        },
         {
             "name": "myclabs/deep-copy",
             "version": "1.13.0",

+ 239 - 0
config/media-library.php

@@ -0,0 +1,239 @@
+<?php
+
+use Spatie\MediaLibrary\Conversions\ImageGenerators\Image;
+use Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf;
+use Spatie\MediaLibrary\Conversions\ImageGenerators\Svg;
+use Spatie\MediaLibrary\Conversions\ImageGenerators\Video;
+use Spatie\MediaLibrary\Conversions\ImageGenerators\Webp;
+use Spatie\MediaLibrary\Conversions\ImageGenerators\Avif;
+use Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob;
+use Spatie\MediaLibrary\Downloaders\DefaultDownloader;
+use Spatie\MediaLibrary\MediaCollections\Models\Media;
+use Spatie\MediaLibrary\MediaCollections\Models\Observers\MediaObserver;
+use Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob;
+use Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred;
+use Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator;
+use Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer;
+use Spatie\MediaLibrary\Support\FileRemover\DefaultFileRemover;
+use Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator;
+use Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator;
+use Spatie\MediaLibraryPro\Models\TemporaryUpload;
+
+return [
+
+    /*
+     * The disk on which to store added files and derived images by default.
+     */
+    'disk_name' => env('MEDIA_DISK', 's3'),
+
+    /*
+     * The disk for conversions when not explicitly specified.
+     */
+    'conversions_disk_name' => env('MEDIA_CONVERSIONS_DISK', null),
+
+    /*
+     * The maximum file size of an item in bytes.
+     */
+    'max_file_size' => 1024 * 1024 * 10,
+
+    /*
+     * Disallowed file extensions.
+     */
+    'disallowed_extensions' => [
+        'php', 'php3', 'php4', 'php5', 'php6', 'php7', 'php8',
+        'phtml', 'phtm', 'pht', 'phps', 'phar',
+        'shtml', 'shtm', 'stm',
+        'htaccess', 'htpasswd',
+        'cgi', 'pl', 'asp', 'aspx', 'jsp', 'jspx',
+    ],
+
+    /*
+     * Allowed file extensions filter.
+     */
+    'allowed_extensions' => null,
+
+    /*
+     * Queue connection for generating derived images.
+     */
+    'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'),
+
+    /*
+     * Queue name for generating derived images.
+     */
+    'queue_name' => env('MEDIA_QUEUE', ''),
+
+    /*
+     * Queue conversions by default.
+     */
+    'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true),
+
+    /*
+     * Should DB transactions be run after DB commits.
+     */
+    'queue_conversions_after_database_commit' => env('QUEUE_CONVERSIONS_AFTER_DB_COMMIT', true),
+
+    /*
+     * The fully qualified class name of the media model.
+     */
+    'media_model' => Media::class,
+
+    /*
+     * The fully qualified class name of the media observer.
+     */
+    'media_observer' => MediaObserver::class,
+
+    /*
+     * Media collection serialization.
+     */
+    'use_default_collection_serialization' => false,
+
+    /*
+     * Temporary upload model (Media Library Pro).
+     */
+    'temporary_upload_model' => TemporaryUpload::class,
+
+    /*
+     * Enable temporary uploads session affinity.
+     */
+    'enable_temporary_uploads_session_affinity' => true,
+
+    /*
+     * Generate thumbnails for temporary uploads.
+     */
+    'generate_thumbnails_for_temporary_uploads' => true,
+
+    /*
+     * File namer class.
+     */
+    'file_namer' => DefaultFileNamer::class,
+
+    /*
+     * Path generator class.
+     */
+    'path_generator' => DefaultPathGenerator::class,
+
+    /*
+     * File remover class.
+     */
+    'file_remover_class' => DefaultFileRemover::class,
+
+    /*
+     * Custom path generators per model.
+     */
+    'custom_path_generators' => [],
+
+    /*
+     * URL generator class.
+     */
+    'url_generator' => DefaultUrlGenerator::class,
+
+    /*
+     * Moves media on update to keep path consistent.
+     */
+    'moves_media_on_update' => false,
+
+    /*
+     * Version urls with ?v=xx query string.
+     */
+    'version_urls' => false,
+
+    /*
+     * Image optimizers.
+     */
+    'image_optimizers' => [
+        // Left empty - optimization handled by project's ImageUpload service
+    ],
+
+    /*
+     * Image generators.
+     */
+    'image_generators' => [
+        Image::class,
+        Webp::class,
+        Avif::class,
+        Pdf::class,
+        Svg::class,
+        Video::class,
+    ],
+
+    /*
+     * Temporary directory path for image conversions.
+     */
+    'temporary_directory_path' => null,
+
+    /*
+     * Image driver: gd, imagick or vips.
+     */
+    'image_driver' => env('IMAGE_DRIVER', 'gd'),
+
+    /*
+     * FFMPEG paths.
+     */
+    'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'),
+    'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'),
+
+    /*
+     * FFMPEG timeout in seconds.
+     */
+    'ffmpeg_timeout' => env('FFMPEG_TIMEOUT', 900),
+
+    /*
+     * FFMPEG threads.
+     */
+    'ffmpeg_threads' => env('FFMPEG_THREADS', 0),
+
+    /*
+     * Jobs used by the package.
+     */
+    'jobs' => [
+        'perform_conversions' => PerformConversionsJob::class,
+        'generate_responsive_images' => GenerateResponsiveImagesJob::class,
+    ],
+
+    /*
+     * Media downloader used by addMediaFromUrl.
+     */
+    'media_downloader' => DefaultDownloader::class,
+
+    /*
+     * SSL verification for remote media downloads.
+     */
+    'media_downloader_ssl' => env('MEDIA_DOWNLOADER_SSL', true),
+
+    /*
+     * Default lifetime in minutes for temporary URLs.
+     */
+    'temporary_url_default_lifetime' => env('MEDIA_TEMPORARY_URL_DEFAULT_LIFETIME', 5),
+
+    'remote' => [
+        'extra_headers' => [
+            'CacheControl' => 'max-age=604800',
+        ],
+    ],
+
+    'responsive_images' => [
+        'width_calculator' => FileSizeOptimizedWidthCalculator::class,
+        'use_tiny_placeholders' => true,
+        'tiny_placeholder_generator' => Blurred::class,
+    ],
+
+    /*
+     * Enable Vapor uploads for Media Library Pro.
+     */
+    'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false),
+
+    /*
+     * Default loading attribute for img tags.
+     */
+    'default_loading_attribute_value' => null,
+
+    /*
+     * Storage prefix for media.
+     */
+    'prefix' => env('MEDIA_PREFIX', ''),
+
+    /*
+     * Force lazy loading.
+     */
+    'force_lazy_loading' => env('FORCE_MEDIA_LIBRARY_LAZY_LOADING', true),
+];

+ 19 - 0
config/visitor.php

@@ -34,4 +34,23 @@ return [
         'jenssegers' => \Shetabit\Visitor\Drivers\JenssegersAgent::class,
         'UAParser'   => \Shetabit\Visitor\Drivers\UAParser::class,
     ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | GeoIP Enrichment
+    |--------------------------------------------------------------------------
+    |
+    | When enabled, visits can be enriched with geolocation data stored
+    | in the "geo_raw" JSON column. This package ships with a default
+    | resolver (stevebauman/location). You may implement your own by
+    | adding it to the drivers array below. Each driver must implement
+    | Shetabit\Visitor\Contracts\GeoIpResolver.
+    |
+    */
+    'geoip'    => false,            // disable enrichment by default
+    'resolver' => 'stevebauman',    // default resolver
+    'resolvers' => [
+        'stevebauman' => \Shetabit\Visitor\Resolvers\GeoIp\SteveBaumanResolver::class,
+        'null'        => \Shetabit\Visitor\Resolvers\GeoIp\NullResolver::class,
+    ],
 ];

+ 16 - 12
packages/Longyi/Core/src/Database/Migrations/2026_04_16_000001_alter_product_variant_images_use_media_id.php

@@ -13,17 +13,18 @@ return new class extends Migration
         Schema::table('product_variant_images', function (Blueprint $table) {
             $existing = $this->existingConstraints();
 
-            if (in_array('product_variant_images_product_image_id_foreign', $existing['fks'])) {
-                $table->dropForeign('product_variant_images_product_image_id_foreign');
+            $tablePrefix = DB::getTablePrefix();
+            if (in_array( $tablePrefix.'product_variant_images_product_image_id_foreign', $existing['fks'])) {
+                $table->dropForeign( $tablePrefix.'product_variant_images_product_image_id_foreign');
             }
-            if (in_array('product_variant_images_product_variant_id_foreign', $existing['fks'])) {
-                $table->dropForeign('product_variant_images_product_variant_id_foreign');
+            if (in_array( $tablePrefix.'product_variant_images_product_variant_id_foreign', $existing['fks'])) {
+                $table->dropForeign( $tablePrefix.'product_variant_images_product_variant_id_foreign');
             }
-            if (in_array('variant_image_unique', $existing['indexes'])) {
-                $table->dropUnique('variant_image_unique');
+            if (in_array( $tablePrefix.'variant_image_unique', $existing['indexes'])) {
+                $table->dropUnique( $tablePrefix.'variant_image_unique');
             }
-            if (in_array('idx_image_id', $existing['indexes'])) {
-                $table->dropIndex('idx_image_id');
+            if (in_array( $tablePrefix.'idx_image_id', $existing['indexes'])) {
+                $table->dropIndex( $tablePrefix.'idx_image_id');
             }
         });
 
@@ -35,7 +36,8 @@ return new class extends Migration
         }
 
         // ── Step 3: ensure media_id is bigint unsigned to match media.id ─────
-        DB::statement('ALTER TABLE product_variant_images MODIFY COLUMN media_id BIGINT UNSIGNED NOT NULL');
+        $tableName = DB::getTablePrefix() . 'product_variant_images';
+        DB::statement("ALTER TABLE `{$tableName}` MODIFY COLUMN media_id BIGINT UNSIGNED NOT NULL");
 
         // ── Step 4: restore constraints ──────────────────────────────────────
         Schema::table('product_variant_images', function (Blueprint $table) {
@@ -63,7 +65,8 @@ return new class extends Migration
             $table->dropIndex('idx_media_id');
         });
 
-        DB::statement('ALTER TABLE product_variant_images MODIFY COLUMN media_id INT UNSIGNED NOT NULL');
+        $tableName = DB::getTablePrefix() . 'product_variant_images';
+        DB::statement("ALTER TABLE `{$tableName}` MODIFY COLUMN media_id INT UNSIGNED NOT NULL");
 
         Schema::table('product_variant_images', function (Blueprint $table) {
             $table->renameColumn('media_id', 'product_image_id');
@@ -82,10 +85,11 @@ return new class extends Migration
 
     private function existingConstraints(): array
     {
+        $tablePrefix = DB::getTablePrefix();
         $fks = DB::select("
             SELECT CONSTRAINT_NAME
             FROM information_schema.TABLE_CONSTRAINTS
-            WHERE TABLE_NAME = 'product_variant_images'
+            WHERE TABLE_NAME = '{$tablePrefix}product_variant_images'
               AND CONSTRAINT_SCHEMA = DATABASE()
               AND CONSTRAINT_TYPE = 'FOREIGN KEY'
         ");
@@ -93,7 +97,7 @@ return new class extends Migration
         $indexes = DB::select("
             SELECT DISTINCT INDEX_NAME
             FROM information_schema.STATISTICS
-            WHERE TABLE_NAME = 'product_variant_images'
+            WHERE TABLE_NAME = '{$tablePrefix}product_variant_images'
               AND TABLE_SCHEMA = DATABASE()
         ");
 

+ 20 - 3
packages/Longyi/Core/src/Repositories/ProductImageRepository.php

@@ -2,6 +2,7 @@
 
 namespace Longyi\Core\Repositories;
 
+use Aws\S3\S3Client;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Str;
@@ -99,14 +100,30 @@ class ProductImageRepository extends BaseProductImageRepository
      */
     protected function storeFile(UploadedFile $file, $product): string
     {
-        $directory = 'product/' . $product->id;
+        $directory = 'uploads/product/' . $product->id;
 
         if (Str::contains($file->getMimeType(), 'image')) {
             $manager = new ImageManager;
             $image   = $manager->make($file)->encode('webp');
             $path    = $directory . '/' . Str::random(40) . '.webp';
-            Storage::put($path, $image);
-
+            //Storage::put($path, $image);
+            $s3Client = new S3Client([
+                'version' => 'latest',
+                'region'  => env('AWS_DEFAULT_REGION'),
+                'credentials' => [
+                    'key'    => env('AWS_ACCESS_KEY_ID'),
+                    'secret' => env('AWS_SECRET_ACCESS_KEY'),
+                ],
+                'http'    => [
+                    'verify' => false,  // 临时禁用 SSL 验证用于测试
+                ],
+            ]);
+             $s3Client->putObject([
+                'Bucket'      => env('AWS_BUCKET'),
+                'Key'         => $path,
+                'Body'        => (string) $image,
+                'ContentType' => 'image/webp',
+            ]);
             return $path;
         }
 

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

+ 111 - 0
packages/Longyi/Gift/src/Console/Commands/ImportGiftCards.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace Longyi\Gift\Console\Commands;
+
+use Illuminate\Console\Command;
+use Longyi\Gift\Models\GiftCards;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Monolog\Logger;
+use Monolog\Handler\StreamHandler;
+
+class ImportGiftCards extends Command
+{
+    protected $signature = 'gift-cards:import {--force}';
+    protected $customLogger = null;
+    protected $logFile = 'gift_import.log';
+
+    public function handle()
+    {
+        set_time_limit(0);
+        $limit = 500;
+        $offset = 0;
+        // 初始化自定义日志
+        $this->initCustomLogger($this->logFile);
+        //用户等级
+        $data = [];
+        do {
+            try {
+                $data = $this->fetchData($offset, $limit);
+                $this->saveDatas($data);
+                $offset = $offset + $limit;
+            } catch (\Exception $e) {
+                $this->logError(sprintf('[GiftCard Import] Error at offset %d: %s', $offset, $e->getMessage()));
+                exit(1);
+            }
+            echo $offset;
+            echo PHP_EOL;
+
+        } while (is_array($data) && !empty($data));
+
+        echo 'gift card import over~~';
+        echo PHP_EOL;
+    }
+
+    protected function fetchData($offset, $limit)
+    {
+        $sql = "select * from giftcard_customer where status=1 and endtime > '" . date('Y-m-d H:i:s') . "' order by gitfcard_id ASC limit {$offset}, {$limit}";
+        return DB::select($sql);
+    }
+    protected function saveDatas($data)
+    {
+        if (!$data) {
+            return false;
+        }
+        foreach ($data as $item) {
+            $this->saveGiftCard($item);
+        }
+    }
+
+    protected function saveGiftCard($item)
+    {
+        GiftCards::addGiftCard(
+            $item->customer_id,
+            $item->giftcard_amount,
+            0,
+            $item->endtime,
+            'activity_26_06_03',
+            '手动迁移'
+        );
+    }
+
+
+    /**
+     * Initialize custom logger with specified file
+     *
+     * @param string $logFile
+     */
+    protected function initCustomLogger(string $logFile)
+    {
+        $this->customLogger = new Logger('gift-card-import');
+        $this->customLogger->pushHandler(new StreamHandler($logFile, Logger::INFO));
+    }
+
+    /**
+     * Log info message
+     *
+     * @param string $message
+     */
+    protected function logInfo(string $message)
+    {
+        if ($this->customLogger) {
+            $this->customLogger->info($message);
+        } else {
+            Log::info($message);
+        }
+    }
+
+    /**
+     * Log error message
+     *
+     * @param string $message
+     */
+    protected function logError(string $message)
+    {
+        if ($this->customLogger) {
+            $this->customLogger->error($message);
+        } else {
+            Log::error($message);
+        }
+    }
+}

+ 65 - 59
packages/Longyi/Gift/src/Http/Controllers/Shop/GiftController.php

@@ -6,16 +6,20 @@ use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 use Illuminate\Http\Response;
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
 use Webkul\Checkout\Facades\Cart;
 use Webkul\Shop\Http\Controllers\Controller;
 use Longyi\Gift\Repositories\GiftCardsRepository;
 use Longyi\Gift\Models\GiftCards;
 use Longyi\Gift\Http\Resources\CustomCartResource;
+use Longyi\RewardPoints\Helpers\ApiResponse;
 
 class GiftController extends Controller
 {
     public function __construct(
-        protected GiftCardsRepository $giftCardRepository
+        protected GiftCardsRepository $giftCardRepository,
+        protected GiftCards $giftCardsModel,
+        protected RewardPointRepository $rewardPointRepository
     ) {
     }
 
@@ -24,6 +28,44 @@ class GiftController extends Controller
         echo 111;exit;
         return view('giftcard::shop.index');
     }
+
+    public function lists(): JsonResponse
+    {
+        $customerId = auth()->user()->id;
+        // 获取用户拥有的所有礼品卡
+        $userGiftCards = GiftCards::where('customer_id', $customerId)
+            ->where('giftcard_status', 1)
+            ->get()
+            ->keyBy('channel');
+        $gifts = $this->giftCardsModel->giftcards();
+        foreach ($gifts as &$gift) {
+            $channel = $gift['channel'];
+            if (isset($userGiftCards[$channel])) {
+                $gift['status'] = true;
+            } else {
+                $gift['status'] = false;
+            }
+        }
+        return ApiResponse::success([
+            'lists' => $gifts,
+            'myPoints' => $this->rewardPointRepository->getCustomerPoints($customerId)
+        ]);
+    }
+
+    public function add(Request $request): JsonResponse
+    {
+        $id = $request->input('id');
+        if (!$id) {
+            return ApiResponse::error('Gift card not found');
+        }
+        $customerId = auth()->user()->id;
+        try {
+            $this->giftCardsModel->add($id, $customerId);
+        } catch (\Exception $e) {
+            return ApiResponse::error($e->getMessage());
+        }
+        return ApiResponse::success('Gift card added successfully');
+    }
     /**
      * Get customer's gift cards list
      */
@@ -37,10 +79,8 @@ class GiftController extends Controller
                 ->where('expirationdate', '>=', now())
                 ->orderBy('created_at', 'desc')
                 ->get();
-
-            return response()->json([
-                'success' => true,
-                'data' => $giftCards->map(function ($giftCard) {
+            return ApiResponse::success(
+                $giftCards->map(function ($giftCard) {
                     return [
                         'id' => $giftCard->id,
                         'giftcard_number' => $giftCard->giftcard_number,
@@ -49,16 +89,12 @@ class GiftController extends Controller
                         'formatted_remaining_giftcard_amount' => core()->formatPrice(core()->convertPrice($giftCard->remaining_giftcard_amount)),
                         'used_giftcard_amount' => $giftCard->used_giftcard_amount,
                         'expirationdate' => $giftCard->expirationdate->format('Y-m-d'),
-                        'giftcard_status' => $giftCard->giftcard_status,
+                        'giftcard_status' => $giftCard->giftcard_status
                     ];
-                }),
-            ]);
+                })
+            );
         } catch (\Exception $e) {
-            return response()->json([
-                'success' => false,
-                'message' => 'Failed to fetch gift cards',
-                'error' => $e->getMessage(),
-            ], Response::HTTP_INTERNAL_SERVER_ERROR);
+            return ApiResponse::error($e->getMessage());
         }
     }
     /**
@@ -75,62 +111,33 @@ class GiftController extends Controller
                ->where('customer_id', $customerId)->first();
            $cart = Cart::getCart();
            if (!$giftCard) {
-               return (new JsonResource([
-                   'data'     => new CustomCartResource (Cart::getCart()),
-                   'message'  => trans('Coupon not found.'),
-               ]))->response()->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
+               return ApiResponse::error('Coupon not found.');
            }
            $remainingGiftcardAmount = core()->convertPrice($giftCard->remaining_giftcard_amount);
            if ($remainingGiftcardAmount <= 0) {
-               return (new JsonResource([
-                   'data'     => new CustomCartResource(Cart::getCart()),
-                   'message'  => trans('Gift card already used.'),
-               ]))->response()->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
+               return ApiResponse::error('Gift card already used.');
            }
            if (!empty($cart->giftcard_amount)) {
-               return (new JsonResource([
-                   'data'     => new CustomCartResource(Cart::getCart()),
-                   'message'  => trans('The shopping cart has been paid with a gift card.'),
-               ]))->response()->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
+               return ApiResponse::error('The shopping cart has been paid with a gift card.');
            }
-
-
            $cartTotal = $cart->grand_total;
            $remainingAmount = $remainingGiftcardAmount - $cartTotal;
-
-           if ($remainingAmount >= 0) {
-               // Apply the gift card to the cart
-               GiftCards::setGiftCardCode($giftCard);
-               Cart::collectTotals();
-
-               return response()->json([
-                   'message' => 'Gift card applied successfully',
-                   'remaining_amount' => $remainingAmount,
-                   'giftcard_number' => $giftCard->giftcard_number,
-               ]);
-           } else {
-               // Apply the gift card to the cart
-               GiftCards::setGiftCardCode($giftCard);
-               Cart::collectTotals();
-               // Gift card amount becomes zero as it's fully used
-               return response()->json([
-                   'message' => 'Gift card applied successfully',
-                   'remaining_amount' => 0,
-                   'giftcard_number' => $giftCard->giftcard_number,
-               ]);
-           }
+           // Apply the gift card to the cart
+           GiftCards::setGiftCardCode($giftCard);
+           Cart::collectTotals();
+           // Gift card amount becomes zero as it's fully used
+           return ApiResponse::success([
+               'remaining_amount' => $remainingAmount >= 0 ? $remainingAmount : 0,
+               'giftcard_number' => $giftCard->giftcard_number,
+           ]);
        } catch (\Exception $e) {
-           return (new JsonResource([
-               'data'    => new CustomCartResource(Cart::getCart()),
-               'message' => trans('error'),
-               'error' => $e->getMessage()
-           ]))->response()->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
+           return ApiResponse::error($e->getMessage());
        }
    }
     /**
      * Remove applied Giftcard from the cart.
      */
-    public function destroyGiftCard(): JsonResource
+    public function destroyGiftCard()
     {
         // Get the cart instance
         $cart = Cart::getCart();
@@ -152,10 +159,9 @@ class GiftController extends Controller
         }
 
         // Return response with updated cart data
-        return new JsonResource([
-            'data'     => new CustomCartResource(Cart::getCart()),
-            'message'  => trans('gift::app.giftcard.remove'),
-        ]);
+        return ApiResponse::success([
+                'data'     => new CustomCartResource(Cart::getCart())
+            ], trans('gift::app.giftcard.remove'));
     }
 
     /**

+ 8 - 8
packages/Longyi/Gift/src/Listeners/InvoiceHandler.php

@@ -21,13 +21,13 @@ class InvoiceHandler
             return;
         }
 
-        // 更新发票的礼品卡信息
-        DB::table('invoices')
-            ->where('id', $invoice->id)
-            ->update([
-                'giftcard_number' => $order->giftcard_number,
-                'giftcard_amount' => $order->giftcard_amount,
-                'base_giftcard_amount' => $order->base_giftcard_amount,
-            ]);
+//        // 更新发票的礼品卡信息
+//        DB::table('invoices')
+//            ->where('id', $invoice->id)
+//            ->update([
+//                'giftcard_number' => $order->giftcard_number,
+//                'giftcard_amount' => $order->giftcard_amount,
+//                'base_giftcard_amount' => $order->base_giftcard_amount,
+//            ]);
     }
 }

+ 132 - 1
packages/Longyi/Gift/src/Models/GiftCards.php

@@ -7,6 +7,11 @@ use Longyi\Gift\Models\GiftCardUsageLog;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Longyi\Gift\Contracts\GiftCards as GiftCardsContract;
+use Longyi\RewardPoints\Config\TransactionType;
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use function Symfony\Component\Translation\t;
+use Illuminate\Support\Facades\DB;
+use Carbon\Carbon;
 
 class GiftCards extends Model implements GiftCardsContract
 {
@@ -105,18 +110,49 @@ class GiftCards extends Model implements GiftCardsContract
                 throw new \Exception('gift num is not enough');
             }
         }
+        $expirationDate = self::calculateExpirationDate($expirationdate);
         static::create([
             'giftcard_number' => self::generateGiftCardNumber(),
             'giftcard_amount' => $giftcardAmount,
             'used_giftcard_amount' => 0,
             'remaining_giftcard_amount' => $giftcardAmount,
             'customer_id' => $customerId,
-            'expirationdate' => now()->addDays($expirationdate),
+            'expirationdate' => $expirationDate,
             'giftcard_status' => 1,
             'channel' => $channel,
             'notes' => $notes
         ]);
     }
+    /**
+     * Calculate expiration date from various input types.
+     *
+     * @param  mixed  $expirationdate  Days (int), date string, or Carbon instance
+     * @return \Carbon\Carbon  Calculated expiration date
+     * @throws \Exception If the input type is invalid
+     */
+    protected static function calculateExpirationDate($expirationdate): Carbon
+    {
+        if ($expirationdate instanceof Carbon) {
+            return $expirationdate;
+        }
+
+        if (is_string($expirationdate)) {
+            try {
+                return Carbon::parse($expirationdate);
+            } catch (\Exception $e) {
+                throw new \Exception("Invalid date format: {$expirationdate}. Use Y-m-d format.");
+            }
+        }
+
+        if (is_int($expirationdate) || is_float($expirationdate)) {
+            if ($expirationdate <= 0) {
+                throw new \Exception('Expiration days must be greater than 0');
+            }
+            return now()->addDays((int) $expirationdate);
+        }
+
+        throw new \Exception('Invalid expiration date type. Must be integer (days), string (Y-m-d), or Carbon instance.');
+    }
     public static function generateGiftCardNumber()
     {
         do {
@@ -132,4 +168,99 @@ class GiftCards extends Model implements GiftCardsContract
 
         return $code;
     }
+
+    public function giftcards()
+    {
+        return [
+            1 => [
+                'id' => 1,
+                'name' => 'Gift Card 1',
+                'amount' => 20.00,
+                'points' => 20,
+                'num' => 0,
+                'expirationdate' => 30,
+                'channel' => 'activity_26_04_21',
+                'notes' => ''
+            ],
+            2 => [
+                'id' => 2,
+                'name' => 'Gift Card 2',
+                'amount' => 30.00,
+                'points' => 30,
+                'num' => 0,
+                'expirationdate' => 30,
+                'channel' => 'activity_26_04_21',
+                'notes' => ''
+            ],
+            3 => [
+                'id' => 3,
+                'name' => 'Gift Card 3',
+                'amount' => 40.00,
+                'points' => 40,
+                'num' => 0,
+                'expirationdate' => 30,
+                'channel' => 'activity_26_04_21',
+                'notes' => ''
+            ],
+            4 => [
+                'id' => 4,
+                'name' => 'Gift Card 4',
+                'amount' => 50.00,
+                'points' => 50,
+                'num' => 0,
+                'expirationdate' => 30,
+                'channel' => 'activity_26_04_21',
+                'notes' => ''
+            ],
+        ];
+    }
+
+    /**
+     * Add a gift card for a customer using points
+     *
+     * @param int $id Gift card ID from giftcards() array
+     * @param int $customerId Customer ID
+     * @return array|true
+     */
+    public function add($id, $customerId)
+    {
+        if (empty($customerId)) {
+            throw new \Exception('customer id is empty');
+        }
+        if (empty($id)) {
+            throw new \Exception('gift card id is empty');
+        }
+        $gifts = $this->giftcards();
+        $giftInfo = $gifts[$id] ?? null;
+        if (!$giftInfo) {
+            throw new \Exception("gift card not found (ID: {$id})");
+        }
+        $points = $giftInfo['points'] ?? 0;
+        if ($points <= 0) {
+            throw new \Exception("gift card points configuration error (points: {$points})");
+        }
+        try {
+            DB::beginTransaction();
+            self::addGiftCard(
+                $customerId,
+                $giftInfo['amount'],
+                $giftInfo['num'],
+                $giftInfo['expirationdate'],
+                $giftInfo['channel'],
+                $giftInfo['notes']
+            );
+            $rewardPointRepository = app(RewardPointRepository::class);
+            $rewardPointRepository->deductPoints(
+                $customerId,
+                $points,
+                TransactionType::GIFT_CARD_REDEEM,
+                'Gift Card Redeem'
+            );
+            DB::commit();
+            return true;
+        } catch (\Exception $e) {
+            DB::rollBack();
+            throw $e;
+        }
+    }
 }

+ 1 - 1
packages/Longyi/Gift/src/Providers/EventServiceProvider.php

@@ -16,7 +16,7 @@ class EventServiceProvider extends ServiceProvider
     {
         Event::listen('checkout.cart.collect.totals.after', 'Longyi\Gift\Listeners\GiftHandler@applyGiftCard');
         Event::listen('checkout.order.save.after', 'Longyi\Gift\Listeners\OrderPlacedHandler@afterPlaceOrder');
-        Event::listen('bagisto.shop.checkout.onepage.summary.coupon.after', function($viewRenderEventManager) {
+        Event::listen('bagisto.shop.checkout.cart.summary.member_discount.after', function($viewRenderEventManager) {
             $viewRenderEventManager->addTemplate('gift::components.giftcard-cartsummary');
         });
         Event::listen('bagisto.shop.customers.account.orders.view.information.discount.after', 'Longyi\Gift\Listeners\OrderViewHandler@renderGiftCardInfo');

+ 12 - 3
packages/Longyi/Gift/src/Repositories/CustomInvoiceRepository.php

@@ -2,6 +2,7 @@
 
 namespace Longyi\Gift\Repositories;
 
+use Illuminate\Support\Facades\Event;
 use Webkul\Sales\Repositories\InvoiceRepository as BaseInvoiceRepository;
 
 class CustomInvoiceRepository extends BaseInvoiceRepository
@@ -13,6 +14,10 @@ class CustomInvoiceRepository extends BaseInvoiceRepository
         $invoice->tax_amount = $invoice->base_tax_amount = 0;
         $invoice->shipping_tax_amount = $invoice->shipping_tax_amount = 0;
         $invoice->discount_amount = $invoice->base_discount_amount = 0;
+        $invoice->giftcard_amount = $invoice->giftcard_amount = 0;
+        $invoice->base_giftcard_amount = $invoice->base_giftcard_amount = 0;
+        $invoice->vip_plus_amount = $invoice->base_vip_plus_amount = 0;
+        $invoice->vip_discount_amount = $invoice->base_vip_discount_amount = 0;
 
         foreach ($invoice->items as $item) {
             $invoice->tax_amount += $item->tax_amount;
@@ -41,6 +46,11 @@ class CustomInvoiceRepository extends BaseInvoiceRepository
         $invoice->base_giftcard_amount += $invoice->order->base_giftcard_amount;
         $invoice->giftcard_number = $invoice->order->giftcard_number;
 
+        $invoice->vip_plus_amount += $invoice->order->vip_plus_amount;
+        $invoice->base_vip_plus_amount += $invoice->order->base_vip_plus_amount;
+        $invoice->vip_discount_amount += $invoice->order->vip_discount_amount;
+        $invoice->base_vip_discount_amount += $invoice->order->base_vip_discount_amount;
+
         if ($invoice->order->shipping_tax_amount) {
             $invoice->shipping_tax_amount = $invoice->order->shipping_tax_amount;
 
@@ -75,11 +85,10 @@ class CustomInvoiceRepository extends BaseInvoiceRepository
             }
         }
 
-        $invoice->grand_total = $invoice->sub_total + $invoice->tax_amount + $invoice->shipping_amount - $invoice->discount_amount - $invoice->giftcard_amount;
-        $invoice->base_grand_total = $invoice->base_sub_total + $invoice->base_tax_amount + $invoice->base_shipping_amount - $invoice->base_discount_amount - $invoice->base_giftcard_amount;
+        $invoice->grand_total = $invoice->sub_total + $invoice->tax_amount + $invoice->shipping_amount - $invoice->discount_amount - $invoice->giftcard_amount + $invoice->vip_plus_amount - $invoice->vip_discount_amount;
+        $invoice->base_grand_total = $invoice->base_sub_total + $invoice->base_tax_amount + $invoice->base_shipping_amount - $invoice->base_discount_amount - $invoice->base_giftcard_amount + $invoice->base_vip_plus_amount - $invoice->base_vip_discount_amount;
 
         $invoice->save();
-
         return $invoice;
     }
 }

+ 10 - 2
packages/Longyi/Gift/src/Repositories/CustomOrderRepository.php

@@ -14,6 +14,8 @@ class CustomOrderRepository extends BaseOrderRepository
         $order->tax_amount_invoiced = $order->base_tax_amount_invoiced = 0;
         $order->discount_invoiced = $order->base_discount_invoiced = 0;
         $order->giftcard_amount = $order->base_giftcard_amount = 0;
+        $order->vip_plus_amount = $order->base_vip_plus_amount = 0;
+        $order->vip_discount_amount = $order->base_vip_discount_amount = 0;
 
         foreach ($order->invoices as $invoice) {
             $order->sub_total_invoiced += $invoice->sub_total;
@@ -30,10 +32,16 @@ class CustomOrderRepository extends BaseOrderRepository
 
             $order->giftcard_amount += $invoice->giftcard_amount;
             $order->base_giftcard_amount += $invoice->base_giftcard_amount;
+
+            $order->vip_plus_amount += $invoice->vip_plus_amount;
+            $order->base_vip_plus_amount += $invoice->base_vip_plus_amount;
+
+            $order->vip_discount_amount += $invoice->vip_discount_amount;
+            $order->base_vip_discount_amount += $invoice->base_vip_discount_amount;
         }
 
-        $order->grand_total_invoiced = $order->sub_total_invoiced + $order->shipping_invoiced + $order->tax_amount_invoiced - $order->discount_invoiced - $order->giftcard_amount;
-        $order->base_grand_total_invoiced = $order->base_sub_total_invoiced + $order->base_shipping_invoiced + $order->base_tax_amount_invoiced - $order->base_discount_invoiced - $order->base_giftcard_amount;
+        $order->grand_total_invoiced = $order->sub_total_invoiced + $order->shipping_invoiced + $order->tax_amount_invoiced - $order->discount_invoiced - $order->giftcard_amount + $order->vip_plus_amount - $order->vip_discount_amount;
+        $order->base_grand_total_invoiced = $order->base_sub_total_invoiced + $order->base_shipping_invoiced + $order->base_tax_amount_invoiced - $order->base_discount_invoiced - $order->base_giftcard_amount + $order->base_vip_plus_amount - $order->base_vip_discount_amount;
 
         // order refund total
         $order->sub_total_refunded = $order->base_sub_total_refunded = 0;

+ 21 - 0
packages/Longyi/Gift/src/Resources/views/sales/invoices/view.blade.php

@@ -297,6 +297,7 @@
                                 @lang('admin::app.sales.invoices.view.summary-discount')
                             </p>
                         @endif
+
                         @if ($order->giftcard_amount > 0)
                             <p class="text-gray-600 dark:text-gray-300 !leading-5 text-sm">
                                 @lang('gift::app.giftcard.giftcard_amount')
@@ -308,6 +309,16 @@
                                 @lang('gift::app.giftcard.giftcard_number')
                             </p>
                         @endif
+                        @if ($order->base_vip_plus_amount > 0)
+                            <p class="text-gray-600 dark:text-gray-300 !leading-5 text-sm">
+                                @lang('member::app.member.discount')
+                            </p>
+                        @endif
+                        @if ($order->base_vip_discount_amount > 0)
+                            <p class="text-gray-600 dark:text-gray-300 !leading-5 text-sm">
+                                @lang('member::app.member.vip_discount')
+                            </p>
+                        @endif
                         <p class="text-base font-semibold !leading-5 text-gray-800 dark:text-white">
                             @lang('admin::app.sales.invoices.view.grand-total')
                         </p>
@@ -377,6 +388,16 @@
                                 {{ ($invoice->giftcard_number) }}
                             </p>
                         @endif
+                        @if ($order->base_vip_plus_amount > 0)
+                            <p class="text-gray-600 dark:text-gray-300 !leading-5">
+                                +  {{ core()->formatBasePrice($invoice->base_vip_plus_amount) }}
+                            </p>
+                        @endif
+                        @if ($order->base_vip_discount_amount > 0)
+                            <p class="text-gray-600 dark:text-gray-300 !leading-5">
+                                -  {{ core()->formatBasePrice($invoice->base_vip_discount_amount) }}
+                            </p>
+                        @endif
                         <!-- Grand Total -->
                         <p class="text-base font-semibold !leading-5 text-gray-800 dark:text-white">
                             {{ core()->formatBasePrice($invoice->base_grand_total) }}

+ 4 - 1
packages/Longyi/Gift/src/Resources/views/sales/orders/view.blade.php

@@ -390,6 +390,9 @@
 
                             {!! view_render_event('bagisto.admin.sales.order.view.discount.before') !!}
 
+                            {!! view_render_event('bagisto.admin.sales.order.view.discount.after') !!}
+
+                            {!! view_render_event('bagisto.admin.sales.order.view.giftcard.before', ['order' => $order]) !!}
                             <!-- Discount -->
                             <div class="flex justify-between w-full gap-x-5">
                                 <p class="!leading-5 text-gray-600 dark:text-gray-300">
@@ -416,7 +419,7 @@
                                 </div>
                             @endif
 
-                            {!! view_render_event('bagisto.admin.sales.order.view.discount.after') !!}
+                            {!! view_render_event('bagisto.admin.sales.order.view.giftcard.after') !!}
 
                             {!! view_render_event('bagisto.admin.sales.order.view.grand-total.before') !!}
 

+ 11 - 8
packages/Longyi/Gift/src/Routes/shop-routes.php

@@ -2,13 +2,16 @@
 
 use Illuminate\Support\Facades\Route;
 use Longyi\Gift\Http\Controllers\Shop\GiftController;
+use Longyi\RewardPoints\Http\Middleware\ApiCustomerAuthenticate;
 
-Route::group(['middleware' => ['web', 'theme', 'locale', 'currency'], 'prefix' => 'gift'], function () {
-    Route::get('', [GiftController::class, 'index'])->name('shop.gift.index');
-});
-Route::group(['middleware' => ['web', 'theme', 'locale', 'currency', 'customer'], 'prefix' => 'gift'], function () {
-    Route::get('/my-gift-cards', [GiftController::class, 'getCustomerGiftCards'])->name('shop.api.checkout.cart.giftcard.list');
-    Route::post('/activate', [GiftController::class, 'activateGiftCard'])->name('shop.api.checkout.cart.giftcard.activate');
-    Route::delete('/remove', [GiftController::class, 'destroyGiftCard'])->name('shop.api.checkout.cart.giftcard.remove');
-    Route::post('/auto-apply-expiring', [GiftController::class, 'autoApplyExpiringGiftCard'])->name('shop.api.checkout.cart.giftcard.auto-apply');
+Route::group(['prefix' => 'api'], function () {
+    Route::group(['middleware' => ['api', ApiCustomerAuthenticate::class], 'prefix' => 'gift'], function () {
+        Route::get('/lists', [GiftController::class, 'lists'])->name('shop.gift.lists');
+        Route::post('/add', [GiftController::class, 'add'])->name('shop.gift.add');
+        Route::get('/my-gift-cards', [GiftController::class, 'getCustomerGiftCards'])->name('shop.api.checkout.cart.giftcard.list');
+        Route::post('/activate', [GiftController::class, 'activateGiftCard'])->name('shop.api.checkout.cart.giftcard.activate');
+        Route::delete('/remove', [GiftController::class, 'destroyGiftCard'])->name('shop.api.checkout.cart.giftcard.remove');
+        Route::post('/auto-apply-expiring', [GiftController::class, 'autoApplyExpiringGiftCard'])->name('shop.api.checkout.cart.giftcard.auto-apply');
+    });
 });
+

+ 633 - 0
packages/Longyi/ImageUpload/FRONTEND_EXAMPLES.md

@@ -0,0 +1,633 @@
+# 前端使用示例
+
+本文档提供 ImageUpload 模块在前端(Shop)的使用示例。
+
+## 目录
+
+- [Vue 组件使用](#vue-组件使用)
+- [JavaScript Fetch API](#javascript-fetch-api)
+- [jQuery AJAX](#jquery-ajax)
+- [React 组件](#react-组件)
+- [实际应用场景](#实际应用场景)
+
+## Vue 组件使用
+
+### 1. 在 Blade 模板中使用
+
+```blade
+{{-- 在 Blade 文件中引入 Vue 组件 --}}
+@extends('shop::layouts.master')
+
+@section('content')
+    <div id="app">
+        <h2>上传头像</h2>
+        
+        {{-- 单张图片上传 --}}
+        <image-upload-component
+            :multiple="false"
+            label="点击或拖拽上传头像"
+            hint="支持 JPG, PNG, GIF 格式,最大 5MB"
+            directory="user/avatars"
+            @uploaded="onAvatarUploaded"
+        ></image-upload-component>
+        
+        <div v-if="avatarUrl">
+            <p>头像预览:</p>
+            <img :src="avatarUrl" alt="Avatar" style="max-width: 200px;">
+        </div>
+        
+        <hr>
+        
+        <h2>上传产品图片</h2>
+        
+        {{-- 多张图片上传 --}}
+        <image-upload-component
+            :multiple="true"
+            label="点击或拖拽上传产品照片"
+            hint="最多上传 5 张图片"
+            :max-files="5"
+            directory="product/images"
+            @uploaded="onProductImagesUploaded"
+        ></image-upload-component>
+        
+        <div v-if="productImages.length > 0">
+            <p>已上传 {{ productImages.length }} 张图片:</p>
+            <div v-for="(img, index) in productImages" :key="index">
+                <img :src="img.url" :alt="'Image ' + (index + 1)" style="max-width: 150px; margin: 5px;">
+            </div>
+        </div>
+    </div>
+@endsection
+
+@push('scripts')
+<script>
+    // 注册全局组件
+    Vue.component('image-upload-component', {
+        template: `@include('imageupload::components.image-upload')`
+    });
+    
+    new Vue({
+        el: '#app',
+        data: {
+            avatarUrl: null,
+            productImages: []
+        },
+        methods: {
+            onAvatarUploaded(data) {
+                console.log('头像上传成功:', data);
+                this.avatarUrl = data.url;
+                
+                // 可以保存到数据库
+                this.saveAvatarToDatabase(data.path);
+            },
+            
+            onProductImagesUploaded(results) {
+                console.log('产品图片上传成功:', results);
+                this.productImages = results.filter(r => r.success);
+                
+                // 可以保存到数据库
+                this.saveProductImages(this.productImages);
+            },
+            
+            async saveAvatarToDatabase(path) {
+                try {
+                    const response = await fetch('/api/customer/profile/update-avatar', {
+                        method: 'POST',
+                        headers: {
+                            'Content-Type': 'application/json',
+                            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+                        },
+                        body: JSON.stringify({ avatar_path: path })
+                    });
+                    
+                    const data = await response.json();
+                    if (data.success) {
+                        alert('头像保存成功');
+                    }
+                } catch (error) {
+                    console.error('保存失败:', error);
+                }
+            },
+            
+            async saveProductImages(images) {
+                // 类似地保存到数据库
+                console.log('保存图片到数据库:', images);
+            }
+        }
+    });
+</script>
+@endpush
+```
+
+### 2. 在 Vue SFC 中使用
+
+```vue
+<template>
+    <div class="profile-page">
+        <h2>个人资料</h2>
+        
+        <div class="avatar-section">
+            <label>头像</label>
+            <ImageUpload
+                :multiple="false"
+                directory="user/avatars"
+                @uploaded="handleAvatarUpload"
+            />
+            <img v-if="avatar" :src="avatar" class="avatar-preview" />
+        </div>
+        
+        <div class="gallery-section">
+            <label>相册</label>
+            <ImageUpload
+                :multiple="true"
+                :max-files="10"
+                directory="user/gallery"
+                @uploaded="handleGalleryUpload"
+            />
+            <div class="gallery-grid">
+                <div v-for="img in gallery" :key="img.id" class="gallery-item">
+                    <img :src="img.url" />
+                    <button @click="deleteImage(img)">删除</button>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import ImageUpload from '@/components/ImageUpload.vue';
+
+export default {
+    components: {
+        ImageUpload
+    },
+    
+    data() {
+        return {
+            avatar: null,
+            gallery: []
+        };
+    },
+    
+    methods: {
+        handleAvatarUpload(data) {
+            this.avatar = data.url;
+            
+            // 保存到后端
+            axios.post('/api/customer/profile/avatar', {
+                path: data.path
+            }).then(response => {
+                this.$toast.success('头像更新成功');
+            });
+        },
+        
+        handleGalleryUpload(results) {
+            const successful = results.filter(r => r.success);
+            this.gallery.push(...successful.map((img, index) => ({
+                id: Date.now() + index,
+                url: img.url,
+                path: img.path
+            })));
+        },
+        
+        async deleteImage(image) {
+            if (!confirm('确定删除这张图片吗?')) return;
+            
+            try {
+                const response = await fetch('/api/image-upload/delete', {
+                    method: 'DELETE',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+                    },
+                    body: JSON.stringify({ path: image.path })
+                });
+                
+                const data = await response.json();
+                if (data.success) {
+                    this.gallery = this.gallery.filter(img => img.id !== image.id);
+                    this.$toast.success('删除成功');
+                }
+            } catch (error) {
+                this.$toast.error('删除失败');
+            }
+        }
+    }
+};
+</script>
+
+<style scoped>
+.avatar-preview {
+    width: 150px;
+    height: 150px;
+    border-radius: 50%;
+    object-fit: cover;
+    margin-top: 10px;
+}
+
+.gallery-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+    gap: 15px;
+    margin-top: 20px;
+}
+
+.gallery-item {
+    position: relative;
+}
+
+.gallery-item img {
+    width: 100%;
+    height: 150px;
+    object-fit: cover;
+    border-radius: 8px;
+}
+
+.gallery-item button {
+    position: absolute;
+    top: 5px;
+    right: 5px;
+    background: red;
+    color: white;
+    border: none;
+    padding: 5px 10px;
+    border-radius: 4px;
+    cursor: pointer;
+}
+</style>
+```
+
+## JavaScript Fetch API
+
+### 基础用法
+
+```javascript
+// 获取 CSRF Token
+function getCsrfToken() {
+    return document.querySelector('meta[name="csrf-token"]')?.content;
+}
+
+// 上传单张图片
+async function uploadSingleImage(file, directory = 'shop/uploads') {
+    const formData = new FormData();
+    formData.append('image', file);
+    formData.append('directory', directory);
+    
+    try {
+        const response = await fetch('/api/image-upload/upload', {
+            method: 'POST',
+            body: formData,
+            headers: {
+                'X-CSRF-TOKEN': getCsrfToken()
+            }
+        });
+        
+        const data = await response.json();
+        
+        if (data.success) {
+            return data.data; // { url, path, ... }
+        } else {
+            throw new Error(data.message);
+        }
+    } catch (error) {
+        console.error('Upload failed:', error);
+        throw error;
+    }
+}
+
+// 上传多张图片
+async function uploadMultipleImages(files, directory = 'shop/uploads') {
+    const formData = new FormData();
+    
+    files.forEach(file => {
+        formData.append('images[]', file);
+    });
+    formData.append('directory', directory);
+    
+    try {
+        const response = await fetch('/api/image-upload/upload-multiple', {
+            method: 'POST',
+            body: formData,
+            headers: {
+                'X-CSRF-TOKEN': getCsrfToken()
+            }
+        });
+        
+        const data = await response.json();
+        
+        if (data.success) {
+            return data.data; // Array of results
+        } else {
+            throw new Error(data.message);
+        }
+    } catch (error) {
+        console.error('Upload failed:', error);
+        throw error;
+    }
+}
+
+// 使用示例
+document.getElementById('imageInput').addEventListener('change', async (e) => {
+    const file = e.target.files[0];
+    if (!file) return;
+    
+    try {
+        const result = await uploadSingleImage(file, 'user/photos');
+        console.log('上传成功:', result.url);
+        
+        // 显示预览
+        const preview = document.getElementById('preview');
+        preview.src = result.url;
+        preview.style.display = 'block';
+        
+    } catch (error) {
+        alert('上传失败: ' + error.message);
+    }
+});
+```
+
+### 带进度条的上传
+
+```javascript
+function uploadWithProgress(file, onProgress) {
+    return new Promise((resolve, reject) => {
+        const formData = new FormData();
+        formData.append('image', file);
+        formData.append('directory', 'shop/uploads');
+        
+        const xhr = new XMLHttpRequest();
+        
+        xhr.upload.addEventListener('progress', (event) => {
+            if (event.lengthComputable) {
+                const percentComplete = (event.loaded / event.total) * 100;
+                onProgress(percentComplete);
+            }
+        });
+        
+        xhr.addEventListener('load', () => {
+            if (xhr.status === 200) {
+                const data = JSON.parse(xhr.responseText);
+                if (data.success) {
+                    resolve(data.data);
+                } else {
+                    reject(new Error(data.message));
+                }
+            } else {
+                reject(new Error('Upload failed'));
+            }
+        });
+        
+        xhr.addEventListener('error', () => {
+            reject(new Error('Network error'));
+        });
+        
+        xhr.open('POST', '/api/image-upload/upload');
+        xhr.setRequestHeader('X-CSRF-TOKEN', getCsrfToken());
+        xhr.send(formData);
+    });
+}
+
+// 使用示例
+const fileInput = document.getElementById('imageInput');
+const progressBar = document.getElementById('progressBar');
+
+fileInput.addEventListener('change', async (e) => {
+    const file = e.target.files[0];
+    if (!file) return;
+    
+    try {
+        const result = await uploadWithProgress(file, (progress) => {
+            progressBar.value = progress;
+            progressBar.textContent = Math.round(progress) + '%';
+        });
+        
+        console.log('上传完成:', result.url);
+        
+    } catch (error) {
+        alert('上传失败: ' + error.message);
+    }
+});
+```
+
+## jQuery AJAX
+
+```javascript
+// 单张图片上传
+function uploadImage(file) {
+    var formData = new FormData();
+    formData.append('image', file);
+    formData.append('directory', 'shop/uploads');
+    
+    $.ajax({
+        url: '/api/image-upload/upload',
+        type: 'POST',
+        data: formData,
+        processData: false,
+        contentType: false,
+        headers: {
+            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
+        },
+        success: function(response) {
+            if (response.success) {
+                console.log('上传成功:', response.data.url);
+                $('#preview').attr('src', response.data.url).show();
+            } else {
+                alert('上传失败: ' + response.message);
+            }
+        },
+        error: function(xhr) {
+            alert('请求失败');
+        }
+    });
+}
+
+// 绑定事件
+$('#imageInput').on('change', function() {
+    var file = this.files[0];
+    if (file) {
+        uploadImage(file);
+    }
+});
+```
+
+## React 组件
+
+```jsx
+import React, { useState } from 'react';
+
+function ImageUpload({ multiple = false, directory = 'shop/uploads', onUpload }) {
+    const [uploading, setUploading] = useState(false);
+    const [preview, setPreview] = useState(null);
+    
+    const handleUpload = async (files) => {
+        setUploading(true);
+        
+        const formData = new FormData();
+        
+        if (multiple) {
+            Array.from(files).forEach(file => {
+                formData.append('images[]', file);
+            });
+        } else {
+            formData.append('image', files[0]);
+        }
+        
+        formData.append('directory', directory);
+        
+        try {
+            const response = await fetch(
+                multiple ? '/api/image-upload/upload-multiple' : '/api/image-upload/upload',
+                {
+                    method: 'POST',
+                    body: formData,
+                    headers: {
+                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content
+                    }
+                }
+            );
+            
+            const data = await response.json();
+            
+            if (data.success) {
+                if (!multiple) {
+                    setPreview(data.data.url);
+                }
+                onUpload(data.data);
+            } else {
+                alert('上传失败: ' + data.message);
+            }
+        } catch (error) {
+            alert('上传失败: ' + error.message);
+        } finally {
+            setUploading(false);
+        }
+    };
+    
+    return (
+        <div className="image-upload">
+            <input
+                type="file"
+                accept="image/*"
+                multiple={multiple}
+                onChange={(e) => handleUpload(e.target.files)}
+                disabled={uploading}
+            />
+            
+            {uploading && <p>上传中...</p>}
+            
+            {preview && !multiple && (
+                <img src={preview} alt="Preview" style={{ maxWidth: '300px' }} />
+            )}
+        </div>
+    );
+}
+
+export default ImageUpload;
+```
+
+## 实际应用场景
+
+### 1. 用户头像上传
+
+```javascript
+// 在用户资料页面
+async function updateAvatar(file) {
+    const result = await uploadSingleImage(file, 'user/avatars');
+    
+    // 更新用户资料
+    await fetch('/api/customer/profile', {
+        method: 'PUT',
+        headers: {
+            'Content-Type': 'application/json',
+            'X-CSRF-TOKEN': getCsrfToken()
+        },
+        body: JSON.stringify({
+            avatar: result.path
+        })
+    });
+    
+    // 更新页面显示
+    document.getElementById('userAvatar').src = result.url;
+}
+```
+
+### 2. 商品评价图片上传
+
+```javascript
+// 在评价表单中
+async function submitReview(reviewData, images) {
+    let imagePaths = [];
+    
+    if (images && images.length > 0) {
+        const uploadResults = await uploadMultipleImages(images, 'review/images');
+        imagePaths = uploadResults
+            .filter(r => r.success)
+            .map(r => r.path);
+    }
+    
+    // 提交评价
+    const response = await fetch('/api/product/review', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+            'X-CSRF-TOKEN': getCsrfToken()
+        },
+        body: JSON.stringify({
+            ...reviewData,
+            images: imagePaths
+        })
+    });
+    
+    return response.json();
+}
+```
+
+### 3. 客服聊天图片发送
+
+```javascript
+// 在聊天界面
+async function sendChatMessage(message, image) {
+    let imageUrl = null;
+    
+    if (image) {
+        const result = await uploadSingleImage(image, 'chat/messages');
+        imageUrl = result.url;
+    }
+    
+    // 发送消息
+    await fetch('/api/chat/send', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+            'X-CSRF-TOKEN': getCsrfToken()
+        },
+        body: JSON.stringify({
+            message: message,
+            image_url: imageUrl
+        })
+    });
+}
+```
+
+## 注意事项
+
+1. **CSRF Token**: 确保在所有请求中包含 CSRF token
+2. **文件大小限制**: 前端验证文件大小,避免上传过大文件
+3. **错误处理**: 妥善处理上传失败的情况
+4. **用户体验**: 显示上传进度和状态反馈
+5. **图片预览**: 上传前显示预览,提升用户体验
+6. **安全性**: 验证文件类型,防止恶意文件上传
+
+## 常见问题
+
+**Q: 如何限制上传的文件类型?**  
+A: 在 input 元素上使用 `accept` 属性:`accept="image/jpeg,image/png,image/gif"`
+
+**Q: 如何实现拖拽上传?**  
+A: 监听 `dragover`, `dragleave`, `drop` 事件,参考 Vue 组件中的实现
+
+**Q: 上传大文件时超时怎么办?**  
+A: 增加服务器超时时间,或实现分片上传
+
+**Q: 如何在移动端优化?**  
+A: 使用 `capture` 属性直接调用相机:`<input type="file" accept="image/*" capture>`

+ 191 - 0
packages/Longyi/ImageUpload/INSTALLATION.md

@@ -0,0 +1,191 @@
+# ImageUpload 模块安装指南
+
+## 快速开始
+
+### 1. 安装依赖
+
+在项目根目录运行:
+
+```bash
+composer dump-autoload
+```
+
+由于使用了本地路径仓库(packages/*/*),composer会自动识别新模块。
+
+### 2. 配置 AWS S3
+
+在 `.env` 文件中添加或修改以下配置:
+
+```env
+# 设置默认上传磁盘为s3
+IMAGE_UPLOAD_DISK=s3
+
+# AWS S3 配置
+AWS_ACCESS_KEY_ID=your-access-key-id
+AWS_SECRET_ACCESS_KEY=your-secret-access-key
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=your-bucket-name
+AWS_URL=https://your-bucket.s3.us-east-1.amazonaws.com
+AWS_USE_PATH_STYLE_ENDPOINT=false
+
+# 可选:设置最大上传大小(KB),默认5120KB (5MB)
+IMAGE_UPLOAD_MAX_SIZE=5120
+```
+
+### 3. 安装AWS S3依赖
+
+如果尚未安装AWS S3 SDK,运行:
+
+```bash
+composer require league/flysystem-aws-s3-v3 "^3.0"
+```
+
+### 4. 清除缓存
+
+```bash
+php artisan config:clear
+php artisan cache:clear
+php artisan route:clear
+php artisan view:clear
+```
+
+### 5. 访问管理后台
+
+登录管理后台后,访问:
+```
+/admin/image-upload
+```
+
+或者在菜单中添加链接到图片上传页面。
+
+### 6. 前端使用
+
+前端用户可以通过 API 上传图片:
+
+- **单张上传**: `POST /api/image-upload/upload`
+- **批量上传**: `POST /api/image-upload/upload-multiple`
+- **删除图片**: `DELETE /api/image-upload/delete`
+- **获取URL**: `GET /api/image-upload/url`
+
+详细的前端使用示例请查看 [FRONTEND_EXAMPLES.md](FRONTEND_EXAMPLES.md)
+
+## 验证安装
+
+### 检查路由是否注册
+
+运行以下命令查看路由:
+
+```bash
+php artisan route:list | grep image-upload
+```
+
+应该看到以下路由:
+- POST   /admin/image-upload/upload
+- POST   /admin/image-upload/upload-multiple
+- DELETE /admin/image-upload/delete
+- GET    /admin/image-upload/url
+
+### 测试上传
+
+1. 访问 `/admin/image-upload`
+2. 选择一张图片
+3. 点击上传按钮
+4. 检查是否成功上传并显示图片URL
+
+## 故障排除
+
+### 问题1: 找不到类错误
+
+如果看到类似 `Class "Longyi\ImageUpload\Services\ImageUploadService" not found` 的错误:
+
+```bash
+composer dump-autoload
+php artisan clear-compiled
+```
+
+### 问题2: S3连接错误
+
+如果遇到S3连接问题:
+
+1. 检查 `.env` 中的AWS配置是否正确
+2. 确认IAM用户有S3写入权限
+3. 检查Bucket策略是否允许访问
+4. 测试AWS凭证:
+
+```bash
+php artisan tinker
+>>> Storage::disk('s3')->put('test.txt', 'test')
+```
+
+### 问题3: 权限错误
+
+确保存储目录有正确的权限:
+
+```bash
+chmod -R 775 storage
+chmod -R 775 bootstrap/cache
+```
+
+### 问题4: 图片上传失败
+
+检查以下内容:
+
+1. PHP上传限制 (`php.ini`):
+   ```ini
+   upload_max_filesize = 10M
+   post_max_size = 10M
+   ```
+
+2. Laravel验证规则中的文件大小限制
+
+3. 查看日志文件:
+   ```bash
+   tail -f storage/logs/laravel.log
+   ```
+
+## 切换到本地存储
+
+如果想使用本地存储而不是S3,修改 `.env`:
+
+```env
+IMAGE_UPLOAD_DISK=public
+```
+
+然后运行:
+
+```bash
+php artisan storage:link
+```
+
+## API测试示例
+
+使用curl测试上传API:
+
+### 单张图片上传
+
+```bash
+curl -X POST http://your-domain.com/admin/image-upload/upload \
+  -H "Authorization: Bearer YOUR_TOKEN" \
+  -F "image=@/path/to/image.jpg" \
+  -F "directory=test/uploads"
+```
+
+### 多张图片上传
+
+```bash
+curl -X POST http://your-domain.com/admin/image-upload/upload-multiple \
+  -H "Authorization: Bearer YOUR_TOKEN" \
+  -F "images[]=@/path/to/image1.jpg" \
+  -F "images[]=@/path/to/image2.jpg" \
+  -F "directory=test/uploads"
+```
+
+## 下一步
+
+- 阅读 [README.md](README.md) 了解完整功能
+- 查看控制器代码了解API使用方法
+- 根据需要自定义配置
+
+## 支持
+
+如有问题,请联系:dev@longyi.com

+ 152 - 0
packages/Longyi/ImageUpload/QUICKSTART.md

@@ -0,0 +1,152 @@
+# ImageUpload 模块 - 快速开始
+
+## 🚀 5分钟快速上手
+
+### 1️⃣ 安装(1分钟)
+
+```bash
+# 自动加载新模块
+composer dump-autoload
+
+# 安装 AWS S3 依赖(如果使用S3)
+composer require league/flysystem-aws-s3-v3 "^3.0"
+```
+
+### 2️⃣ 配置(1分钟)
+
+在 `.env` 文件中添加:
+
+```env
+# 使用 S3 存储
+IMAGE_UPLOAD_DISK=s3
+
+AWS_ACCESS_KEY_ID=your-key
+AWS_SECRET_ACCESS_KEY=your-secret
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=your-bucket
+```
+
+或使用本地存储:
+
+```env
+IMAGE_UPLOAD_DISK=public
+```
+
+### 3️⃣ 清除缓存(30秒)
+
+```bash
+php artisan config:clear
+php artisan cache:clear
+```
+
+### 4️⃣ 开始使用(3分钟)
+
+#### 管理后台
+
+访问:`/admin/image-upload`
+
+#### 前端 API
+
+```javascript
+// 上传单张图片
+const formData = new FormData();
+formData.append('image', fileInput.files[0]);
+formData.append('directory', 'shop/uploads');
+
+fetch('/api/image-upload/upload', {
+    method: 'POST',
+    body: formData,
+    headers: {
+        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+    }
+})
+.then(res => res.json())
+.then(data => {
+    if (data.success) {
+        console.log('图片URL:', data.data.url);
+    }
+});
+```
+
+#### Vue 组件
+
+```vue
+<template>
+    <image-upload
+        :multiple="false"
+        directory="user/avatars"
+        @uploaded="handleUpload"
+    />
+</template>
+
+<script>
+export default {
+    methods: {
+        handleUpload(data) {
+            console.log('上传成功:', data.url);
+        }
+    }
+}
+</script>
+```
+
+## 📚 更多资源
+
+- [完整文档](README.md) - 详细的功能说明和API参考
+- [安装指南](INSTALLATION.md) - 详细的安装步骤和故障排除
+- [前端示例](FRONTEND_EXAMPLES.md) - Vue、React、JavaScript等使用示例
+
+## ✨ 核心功能
+
+✅ 单张/批量图片上传  
+✅ AWS S3 / 本地存储  
+✅ 自动图片优化(WebP)  
+✅ 文件验证(类型、大小)  
+✅ 管理后台 + 前端支持  
+✅ Vue 组件即用  
+✅ RESTful API  
+
+## 🔗 API 路由
+
+| 用途 | 管理后台 | 前端 |
+|------|---------|------|
+| 上传单张 | `POST /admin/image-upload/upload` | `POST /api/image-upload/upload` |
+| 上传多张 | `POST /admin/image-upload/upload-multiple` | `POST /api/image-upload/upload-multiple` |
+| 删除图片 | `DELETE /admin/image-upload/delete` | `DELETE /api/image-upload/delete` |
+| 获取URL | `GET /admin/image-upload/url` | `GET /api/image-upload/url` |
+
+## 💡 常见用例
+
+### 用户头像
+```javascript
+uploadSingleImage(file, 'user/avatars')
+```
+
+### 产品图片
+```javascript
+uploadMultipleImages(files, 'product/images')
+```
+
+### 评价图片
+```javascript
+uploadMultipleImages(files, 'review/images')
+```
+
+### 聊天消息
+```javascript
+uploadSingleImage(file, 'chat/messages')
+```
+
+## ❓ 遇到问题?
+
+1. 检查 [.env](file://d:/work/bagisto/.env) 配置是否正确
+2. 查看 [安装指南](INSTALLATION.md) 的故障排除部分
+3. 检查日志:`storage/logs/laravel.log`
+
+## 🎉 完成!
+
+现在你可以在管理后台和前端使用图片上传功能了!
+
+---
+
+**需要帮助?** 查看完整文档或联系 dev@longyi.com

+ 507 - 0
packages/Longyi/ImageUpload/README.md

@@ -0,0 +1,507 @@
+# Longyi Image Upload Module
+
+一个用于Bagisto的图片上传模块,支持上传到AWS S3或其他存储驱动。
+
+## 功能特性
+
+- ✅ 支持单张图片上传
+- ✅ 支持多张图片批量上传
+- ✅ 支持AWS S3存储
+- ✅ 支持本地存储
+- ✅ 自动图片优化(转换为WebP格式)
+- ✅ 文件大小和类型验证
+- ✅ 自定义上传目录
+- ✅ 图片删除功能
+- ✅ 中英文界面支持
+- ✅ **管理后台和前端都支持**
+- ✅ **提供Vue组件供前端使用**
+
+## 安装
+
+### 1. 添加依赖
+
+在项目的根目录运行:
+
+```bash
+composer require longyi/image-upload
+```
+
+或者手动添加到 `composer.json`:
+
+```json
+{
+    "require": {
+        "longyi/image-upload": "^1.0"
+    }
+}
+```
+
+### 2. 配置AWS S3(如果使用S3存储)
+
+在 `.env` 文件中添加以下配置:
+
+```env
+# 设置默认上传磁盘为s3
+IMAGE_UPLOAD_DISK=s3
+
+# AWS S3 配置
+AWS_ACCESS_KEY_ID=your-access-key-id
+AWS_SECRET_ACCESS_KEY=your-secret-access-key
+AWS_DEFAULT_REGION=your-region
+AWS_BUCKET=your-bucket-name
+AWS_URL=https://your-bucket.s3.your-region.amazonaws.com
+AWS_USE_PATH_STYLE_ENDPOINT=false
+
+# 可选:设置最大上传大小(KB)
+IMAGE_UPLOAD_MAX_SIZE=5120
+```
+
+### 3. 发布配置文件(可选)
+
+```bash
+php artisan vendor:publish --provider="Longyi\ImageUpload\Providers\ImageUploadServiceProvider" --tag=config
+```
+
+### 4. 清除缓存
+
+```bash
+php artisan config:clear
+php artisan cache:clear
+```
+
+## 使用方法
+
+### 管理后台访问
+
+访问管理后台的图片上传页面:
+```
+/admin/image-upload
+```
+
+### 前端 API 使用
+
+前端用户可以通过 API 上传图片,所有接口都在 `/api/image-upload` 路径下。
+
+#### 1. 上传单张图片(前端)
+
+**请求:**
+```http
+POST /api/image-upload/upload
+Content-Type: multipart/form-data
+
+image: [file]
+directory: shop/uploads (可选,默认为 shop/uploads)
+```
+
+**响应:**
+```json
+{
+    "success": true,
+    "message": "Image uploaded successfully",
+    "data": {
+        "success": true,
+        "path": "shop/uploads/2026/05/27/abc123.webp",
+        "url": "https://bucket.s3.region.amazonaws.com/shop/uploads/2026/05/27/abc123.webp",
+        "original_name": "photo.jpg",
+        "mime_type": "image/jpeg",
+        "size": 102400,
+        "disk": "s3"
+    }
+}
+```
+
+#### 2. 上传多张图片(前端)
+
+**请求:**
+```http
+POST /api/image-upload/upload-multiple
+Content-Type: multipart/form-data
+
+images[]: [file1]
+images[]: [file2]
+directory: shop/uploads (可选)
+```
+
+**限制:**
+- 最多上传 10 张图片
+- 每张不超过配置的最大大小
+
+### Vue 组件使用
+
+模块提供了一个现成的 Vue 组件,可以在前端页面中使用:
+
+```vue
+<template>
+    <div>
+        <!-- 单张图片上传 -->
+        <image-upload
+            :multiple="false"
+            label="点击或拖拽上传头像"
+            directory="user/avatars"
+            @uploaded="handleUploaded"
+        />
+        
+        <!-- 多张图片上传 -->
+        <image-upload
+            :multiple="true"
+            label="点击或拖拽上传产品图片"
+            :max-files="5"
+            directory="product/images"
+            @uploaded="handleMultipleUploaded"
+        />
+    </div>
+</template>
+
+<script>
+import ImageUpload from './components/image-upload.blade.php';
+
+export default {
+    components: {
+        ImageUpload
+    },
+    
+    methods: {
+        handleUploaded(data) {
+            console.log('上传成功:', data);
+            // data.url - 图片URL
+            // data.path - 存储路径
+        },
+        
+        handleMultipleUploaded(results) {
+            console.log('批量上传结果:', results);
+            results.forEach(result => {
+                if (result.success) {
+                    console.log('图片URL:', result.url);
+                }
+            });
+        }
+    }
+};
+</script>
+```
+
+### JavaScript/Fetch 使用示例
+
+如果你不使用 Vue,也可以直接用 JavaScript 调用 API:
+
+```javascript
+// 单张图片上传
+async function uploadImage(file) {
+    const formData = new FormData();
+    formData.append('image', file);
+    formData.append('directory', 'custom/path');
+    
+    try {
+        const response = await fetch('/api/image-upload/upload', {
+            method: 'POST',
+            body: formData,
+            headers: {
+                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content
+            }
+        });
+        
+        const data = await response.json();
+        
+        if (data.success) {
+            console.log('上传成功:', data.data.url);
+            return data.data;
+        } else {
+            console.error('上传失败:', data.message);
+        }
+    } catch (error) {
+        console.error('错误:', error);
+    }
+}
+
+// 多张图片上传
+async function uploadMultipleImages(files) {
+    const formData = new FormData();
+    files.forEach(file => {
+        formData.append('images[]', file);
+    });
+    formData.append('directory', 'custom/path');
+    
+    try {
+        const response = await fetch('/api/image-upload/upload-multiple', {
+            method: 'POST',
+            body: formData,
+            headers: {
+                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content
+            }
+        });
+        
+        const data = await response.json();
+        
+        if (data.success) {
+            console.log('上传成功:', data.data);
+            return data.data;
+        } else {
+            console.error('上传失败:', data.message);
+        }
+    } catch (error) {
+        console.error('错误:', error);
+    }
+}
+
+// 使用示例
+document.getElementById('imageInput').addEventListener('change', async (e) => {
+    const file = e.target.files[0];
+    if (file) {
+        const result = await uploadImage(file);
+        // 使用 result.url 显示图片或保存到数据库
+        document.getElementById('preview').src = result.url;
+    }
+});
+```
+
+### 管理后台 API 使用
+
+以下 API 端点用于管理后台(需要管理员权限):
+
+#### 1. 上传单张图片(管理后台)
+
+**请求:**
+```http
+POST /admin/image-upload/upload
+Content-Type: multipart/form-data
+
+image: [file]
+directory: custom/path (可选)
+```
+
+**响应:**
+```json
+{
+    "success": true,
+    "message": "Image uploaded successfully",
+    "data": {
+        "success": true,
+        "path": "images/uploads/2026/05/27/abc123.webp",
+        "url": "https://bucket.s3.region.amazonaws.com/images/uploads/2026/05/27/abc123.webp",
+        "original_name": "photo.jpg",
+        "mime_type": "image/jpeg",
+        "size": 102400,
+        "disk": "s3"
+    }
+}
+```
+
+#### 2. 上传多张图片(管理后台)
+
+**请求:**
+```http
+POST /admin/image-upload/upload-multiple
+Content-Type: multipart/form-data
+
+images[]: [file1]
+images[]: [file2]
+directory: custom/path (可选)
+```
+
+**响应:**
+```json
+{
+    "success": true,
+    "message": "2 images uploaded successfully, 0 failed",
+    "data": [
+        {
+            "success": true,
+            "path": "images/uploads/2026/05/27/abc123.webp",
+            "url": "https://...",
+            "original_name": "photo1.jpg",
+            "mime_type": "image/jpeg",
+            "size": 102400,
+            "disk": "s3"
+        },
+        {
+            "success": true,
+            "path": "images/uploads/2026/05/27/def456.webp",
+            "url": "https://...",
+            "original_name": "photo2.jpg",
+            "mime_type": "image/jpeg",
+            "size": 204800,
+            "disk": "s3"
+        }
+    ]
+}
+```
+
+#### 3. 删除图片(管理后台)
+
+**请求:**
+```http
+DELETE /admin/image-upload/delete
+Content-Type: application/json
+
+{
+    "path": "images/uploads/2026/05/27/abc123.webp",
+    "disk": "s3" (可选)
+}
+```
+
+**响应:**
+```json
+{
+    "success": true,
+    "message": "Image deleted successfully"
+}
+```
+
+#### 4. 获取图片URL(管理后台)
+
+**请求:**
+```http
+GET /admin/image-upload/url?path=images/uploads/2026/05/27/abc123.webp&disk=s3
+```
+
+**响应:**
+```json
+{
+    "success": true,
+    "url": "https://bucket.s3.region.amazonaws.com/images/uploads/2026/05/27/abc123.webp"
+}
+```
+
+### API 路由对比
+
+| 功能 | 管理后台路由 | 前端路由 | 说明 |
+|------|------------|---------|------|
+| 上传单张 | POST `/admin/image-upload/upload` | POST `/api/image-upload/upload` | 前端默认目录为 `shop/uploads` |
+| 上传多张 | POST `/admin/image-upload/upload-multiple` | POST `/api/image-upload/upload-multiple` | 前端最多10张 |
+| 删除图片 | DELETE `/admin/image-upload/delete` | DELETE `/api/image-upload/delete` | - |
+| 获取URL | GET `/admin/image-upload/url` | GET `/api/image-upload/url` | - |
+
+**主要区别:**
+- **管理后台**:需要管理员权限,路径为 `/admin/*`
+- **前端**:无需特殊权限(或需要登录),路径为 `/api/*`,默认上传到 `shop/uploads` 目录
+
+## 代码中使用
+
+```php
+use Longyi\ImageUpload\Services\ImageUploadService;
+
+// 注入服务
+public function __construct(
+    protected ImageUploadService $imageUploadService
+) {}
+
+// 上传单张图片
+public function uploadImage(Request $request)
+{
+    $file = $request->file('image');
+    $result = $this->imageUploadService->upload($file, 'custom/directory');
+    
+    if ($result['success']) {
+        // 使用 $result['url'] 或 $result['path']
+    }
+}
+
+// 上传多张图片
+public function uploadImages(Request $request)
+{
+    $files = $request->file('images');
+    $results = $this->imageUploadService->uploadMultiple($files, 'custom/directory');
+    
+    foreach ($results as $result) {
+        if ($result['success']) {
+            // 处理成功的上传
+        }
+    }
+}
+
+// 删除图片
+public function deleteImage(string $path)
+{
+    $deleted = $this->imageUploadService->delete($path);
+    
+    if ($deleted) {
+        // 删除成功
+    }
+}
+
+// 获取图片URL
+public function getImageUrl(string $path)
+{
+    $url = $this->imageUploadService->getUrl($path);
+    
+    return $url;
+}
+```
+
+## 配置说明
+
+配置文件位于 `config/image_upload.php`:
+
+```php
+return [
+    // 默认存储磁盘:'local' 或 's3'
+    'default_disk' => env('IMAGE_UPLOAD_DISK', 's3'),
+
+    // 允许的图片类型
+    'allowed_types' => [
+        'image/jpeg',
+        'image/jpg',
+        'image/png',
+        'image/gif',
+        'image/webp',
+        'image/svg+xml',
+    ],
+
+    // 最大文件大小(KB)
+    'max_size' => env('IMAGE_UPLOAD_MAX_SIZE', 5120),
+
+    // 上传目录
+    'upload_directory' => 'images/uploads',
+
+    // S3配置
+    's3' => [
+        'bucket' => env('AWS_BUCKET'),
+        'region' => env('AWS_DEFAULT_REGION'),
+        'visibility' => 'public',
+    ],
+];
+```
+
+## 扩展其他存储方式
+
+要添加其他存储方式(如阿里云OSS、腾讯云COS等),只需:
+
+1. 安装相应的Flysystem适配器
+2. 在 `config/filesystems.php` 中配置新的disk
+3. 修改 `IMAGE_UPLOAD_DISK` 环境变量或使用新的disk名称
+
+例如,添加阿里云OSS:
+
+```bash
+composer require aliyuncs/oss-sdk-php
+composer require jacobcyl/ali-oss-storage
+```
+
+在 `config/filesystems.php` 中添加:
+
+```php
+'oss' => [
+    'driver' => 'oss',
+    'access_id' => env('ALIYUN_ACCESS_KEY_ID'),
+    'access_key' => env('ALIYUN_ACCESS_KEY_SECRET'),
+    'bucket' => env('ALIYUN_BUCKET'),
+    'endpoint' => env('ALIYUN_ENDPOINT'),
+],
+```
+
+然后设置 `IMAGE_UPLOAD_DISK=oss` 即可。
+
+## 依赖要求
+
+- PHP >= 8.1
+- Laravel >= 10.0
+- intervention/image
+- league/flysystem-aws-s3-v3 (用于S3支持)
+
+## 许可证
+
+MIT License
+
+## 作者
+
+Longyi Team <dev@longyi.com>

+ 31 - 0
packages/Longyi/ImageUpload/composer.json

@@ -0,0 +1,31 @@
+{
+    "name": "longyi/image-upload",
+    "description": "Longyi Image Upload Module for Bagisto - Support uploading images to AWS S3",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Longyi Team",
+            "email": "dev@longyi.com"
+        }
+    ],
+    "require": {
+        "php": "^8.1|^8.2",
+        "illuminate/support": "^10.0|^11.0",
+        "league/flysystem-aws-s3-v3": "^3.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Longyi\\ImageUpload\\": "src/"
+        }
+    },
+    "extra": {
+        "laravel": {
+            "providers": [
+                "Longyi\\ImageUpload\\Providers\\ImageUploadServiceProvider"
+            ]
+        }
+    },
+    "minimum-stability": "dev",
+    "prefer-stable": true
+}

+ 65 - 0
packages/Longyi/ImageUpload/src/Config/image_upload.php

@@ -0,0 +1,65 @@
+<?php
+
+return [
+    /*
+    |--------------------------------------------------------------------------
+    | Default Upload Disk
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify the default filesystem disk that should be used
+    | for image uploads. Options: 'local', 's3'
+    |
+    */
+    'default_disk' => env('IMAGE_UPLOAD_DISK', 's3'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Allowed Image Types
+    |--------------------------------------------------------------------------
+    |
+    | The allowed image MIME types for upload.
+    |
+    */
+    'allowed_types' => [
+        'image/jpeg',
+        'image/jpg',
+        'image/png',
+        'image/gif',
+        'image/webp',
+        'image/svg+xml',
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Maximum File Size
+    |--------------------------------------------------------------------------
+    |
+    | The maximum file size in kilobytes (KB).
+    |
+    */
+    'max_size' => env('IMAGE_UPLOAD_MAX_SIZE', 5120), // 5MB default
+
+    /*
+    |--------------------------------------------------------------------------
+    | Upload Directory
+    |--------------------------------------------------------------------------
+    |
+    | The directory where images will be stored.
+    |
+    */
+    'upload_directory' => 'images/uploads',
+
+    /*
+    |--------------------------------------------------------------------------
+    | S3 Configuration
+    |--------------------------------------------------------------------------
+    |
+    | AWS S3 specific configuration for image uploads.
+    |
+    */
+    's3' => [
+        'bucket' => env('AWS_BUCKET'),
+        'region' => env('AWS_DEFAULT_REGION'),
+        'visibility' => 'public',
+    ],
+];

+ 166 - 0
packages/Longyi/ImageUpload/src/Http/Controllers/Admin/ImageUploadController.php

@@ -0,0 +1,166 @@
+<?php
+
+namespace Longyi\ImageUpload\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Longyi\ImageUpload\Services\ImageUploadService;
+use Exception;
+
+class ImageUploadController extends Controller
+{
+    /**
+     * @var ImageUploadService
+     */
+    protected $imageUploadService;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @param ImageUploadService $imageUploadService
+     */
+    public function __construct(ImageUploadService $imageUploadService)
+    {
+        $this->imageUploadService = $imageUploadService;
+    }
+
+    /**
+     * Upload a single image.
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function upload(Request $request)
+    {
+        try {
+            $request->validate([
+                'image' => 'required|file',
+                'directory' => 'nullable|string',
+            ]);
+
+            $file = $request->file('image');
+            $directory = $request->input('directory');
+
+            $result = $this->imageUploadService->upload($file, $directory);
+
+            return response()->json([
+                'success' => true,
+                'message' => __('Image uploaded successfully'),
+                'data' => $result,
+            ]);
+        } catch (Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage(),
+            ], 422);
+        }
+    }
+
+    /**
+     * Upload multiple images.
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function uploadMultiple(Request $request)
+    {
+        try {
+            $request->validate([
+                'images' => 'required|array',
+                'images.*' => 'file',
+                'directory' => 'nullable|string',
+            ]);
+
+            $files = $request->file('images');
+            $directory = $request->input('directory');
+
+            $results = $this->imageUploadService->uploadMultiple($files, $directory);
+
+            $successCount = count(array_filter($results, fn($r) => $r['success'] ?? false));
+            $failedCount = count($results) - $successCount;
+
+            return response()->json([
+                'success' => true,
+                'message' => __(':success images uploaded successfully, :failed failed', [
+                    'success' => $successCount,
+                    'failed' => $failedCount,
+                ]),
+                'data' => $results,
+            ]);
+        } catch (Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage(),
+            ], 422);
+        }
+    }
+
+    /**
+     * Delete an image.
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function delete(Request $request)
+    {
+        try {
+            $request->validate([
+                'path' => 'required|string',
+                'disk' => 'nullable|string',
+            ]);
+
+            $path = $request->input('path');
+            $disk = $request->input('disk');
+
+            $deleted = $this->imageUploadService->delete($path, $disk);
+
+            if ($deleted) {
+                return response()->json([
+                    'success' => true,
+                    'message' => __('Image deleted successfully'),
+                ]);
+            } else {
+                return response()->json([
+                    'success' => false,
+                    'message' => __('Failed to delete image'),
+                ], 422);
+            }
+        } catch (Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage(),
+            ], 422);
+        }
+    }
+
+    /**
+     * Get image URL.
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function getUrl(Request $request)
+    {
+        try {
+            $request->validate([
+                'path' => 'required|string',
+                'disk' => 'nullable|string',
+            ]);
+
+            $path = $request->input('path');
+            $disk = $request->input('disk');
+
+            $url = $this->imageUploadService->getUrl($path, $disk);
+
+            return response()->json([
+                'success' => true,
+                'url' => $url,
+            ]);
+        } catch (Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage(),
+            ], 422);
+        }
+    }
+}

+ 169 - 0
packages/Longyi/ImageUpload/src/Http/Controllers/Shop/ImageUploadController.php

@@ -0,0 +1,169 @@
+<?php
+
+namespace Longyi\ImageUpload\Http\Controllers\Shop;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Longyi\ImageUpload\Services\ImageUploadService;
+use Exception;
+
+class ImageUploadController extends Controller
+{
+    /**
+     * @var ImageUploadService
+     */
+    protected $imageUploadService;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @param ImageUploadService $imageUploadService
+     */
+    public function __construct(ImageUploadService $imageUploadService)
+    {
+        $this->imageUploadService = $imageUploadService;
+    }
+
+    /**
+     * Upload a single image (for shop/frontend users).
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function upload(Request $request)
+    {
+        try {
+            $request->validate([
+                'image' => 'required|file|mimes:jpeg,jpg,png,gif,webp,svg|max:' . config('image_upload.max_size', 5120),
+                'directory' => 'nullable|string',
+            ]);
+
+            $file = $request->file('image');
+            $directory = $request->input('directory', 'shop/uploads');
+
+            $result = $this->imageUploadService->upload($file, $directory);
+
+            return response()->json([
+                'success' => true,
+                'message' => __('Image uploaded successfully'),
+                'data' => $result,
+            ]);
+        } catch (Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage(),
+            ], 422);
+        }
+    }
+
+    /**
+     * Upload multiple images (for shop/frontend users).
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function uploadMultiple(Request $request)
+    {
+        try {
+            $request->validate([
+                'images' => 'required|array|min:1|max:10',
+                'images.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg|max:' . config('image_upload.max_size', 5120),
+                'directory' => 'nullable|string',
+            ]);
+
+            $files = $request->file('images');
+            $directory = $request->input('directory', 'shop/uploads');
+
+            $results = $this->imageUploadService->uploadMultiple($files, $directory);
+
+            $successCount = count(array_filter($results, fn($r) => $r['success'] ?? false));
+            $failedCount = count($results) - $successCount;
+
+            return response()->json([
+                'success' => true,
+                'message' => __(':success images uploaded successfully, :failed failed', [
+                    'success' => $successCount,
+                    'failed' => $failedCount,
+                ]),
+                'data' => $results,
+            ]);
+        } catch (Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage(),
+            ], 422);
+        }
+    }
+
+    /**
+     * Delete an image (for shop/frontend users).
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function delete(Request $request)
+    {
+        try {
+            $request->validate([
+                'path' => 'required|string',
+                'disk' => 'nullable|string',
+            ]);
+
+            $path = $request->input('path');
+            $disk = $request->input('disk');
+
+            // Optional: Add authorization check to ensure user can only delete their own images
+            // This depends on your application's requirements
+
+            $deleted = $this->imageUploadService->delete($path, $disk);
+
+            if ($deleted) {
+                return response()->json([
+                    'success' => true,
+                    'message' => __('Image deleted successfully'),
+                ]);
+            } else {
+                return response()->json([
+                    'success' => false,
+                    'message' => __('Failed to delete image'),
+                ], 422);
+            }
+        } catch (Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage(),
+            ], 422);
+        }
+    }
+
+    /**
+     * Get image URL (for shop/frontend users).
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function getUrl(Request $request)
+    {
+        try {
+            $request->validate([
+                'path' => 'required|string',
+                'disk' => 'nullable|string',
+            ]);
+
+            $path = $request->input('path');
+            $disk = $request->input('disk');
+
+            $url = $this->imageUploadService->getUrl($path, $disk);
+
+            return response()->json([
+                'success' => true,
+                'url' => $url,
+            ]);
+        } catch (Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage(),
+            ], 422);
+        }
+    }
+}

+ 62 - 0
packages/Longyi/ImageUpload/src/Providers/ImageUploadServiceProvider.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace Longyi\ImageUpload\Providers;
+
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Facades\Route;
+use Illuminate\Support\ServiceProvider;
+
+class ImageUploadServiceProvider extends ServiceProvider
+{
+    /**
+     * Bootstrap services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        // Load routes
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/shop-routes.php');
+
+        // Load views
+        $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'imageupload');
+
+        // Load translations
+        $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'imageupload');
+
+        // Publish config
+        $this->publishes([
+            __DIR__ . '/../Config/image_upload.php' => config_path('image_upload.php'),
+        ], 'config');
+
+        // Publish views
+        $this->publishes([
+            __DIR__ . '/../Resources/views' => resource_path('views/vendor/imageupload'),
+        ], 'views');
+
+        // Publish translations
+        $this->publishes([
+            __DIR__ . '/../Resources/lang' => resource_path('lang/vendor/imageupload'),
+        ], 'translations');
+    }
+
+    /**
+     * Register services.
+     *
+     * @return void
+     */
+    public function register()
+    {
+        // Merge config
+        $this->mergeConfigFrom(
+            __DIR__ . '/../Config/image_upload.php',
+            'image_upload'
+        );
+
+        // Register the image upload service
+        $this->app->singleton('imageupload', function () {
+            return new \Longyi\ImageUpload\Services\ImageUploadService();
+        });
+    }
+}

+ 25 - 0
packages/Longyi/ImageUpload/src/Resources/lang/en/app.php

@@ -0,0 +1,25 @@
+<?php
+
+return [
+    'admin' => [
+        'image-upload' => [
+            'title' => 'Image Upload',
+            'single_upload' => 'Single Image Upload',
+            'multiple_upload' => 'Multiple Images Upload',
+            'select_image' => 'Select Image',
+            'select_images' => 'Select Images',
+            'directory' => 'Directory',
+            'optional' => 'Optional',
+            'upload' => 'Upload',
+            'upload_multiple' => 'Upload Multiple',
+            'uploaded_images' => 'Uploaded Images',
+            'no_images_uploaded' => 'No images uploaded yet',
+            'success' => 'Success',
+            'error' => 'Error',
+            'delete' => 'Delete',
+            'confirm_delete' => 'Are you sure you want to delete this image?',
+            'delete_success' => 'Image deleted successfully',
+            'delete_failed' => 'Failed to delete image',
+        ],
+    ],
+];

+ 25 - 0
packages/Longyi/ImageUpload/src/Resources/lang/zh_CN/app.php

@@ -0,0 +1,25 @@
+<?php
+
+return [
+    'admin' => [
+        'image-upload' => [
+            'title' => '图片上传',
+            'single_upload' => '单张图片上传',
+            'multiple_upload' => '多张图片上传',
+            'select_image' => '选择图片',
+            'select_images' => '选择图片',
+            'directory' => '目录',
+            'optional' => '可选',
+            'upload' => '上传',
+            'upload_multiple' => '批量上传',
+            'uploaded_images' => '已上传图片',
+            'no_images_uploaded' => '暂无上传图片',
+            'success' => '成功',
+            'error' => '错误',
+            'delete' => '删除',
+            'confirm_delete' => '确定要删除这张图片吗?',
+            'delete_success' => '图片删除成功',
+            'delete_failed' => '图片删除失败',
+        ],
+    ],
+];

+ 291 - 0
packages/Longyi/ImageUpload/src/Resources/views/admin/image-upload/index.blade.php

@@ -0,0 +1,291 @@
+@extends('admin::layouts.master')
+
+@section('page_title')
+    {{ __('imageupload::app.admin.image-upload.title') }}
+@endsection
+
+@section('content-wrapper')
+    <div class="inner-section">
+        <div class="page-header">
+            <div class="page-title">
+                <h1>{{ __('imageupload::app.admin.image-upload.title') }}</h1>
+            </div>
+        </div>
+
+        <div class="page-content">
+            <div class="grid grid-cols-2 gap-4">
+                <!-- Single Image Upload -->
+                <div class="bg-white rounded-lg shadow p-6">
+                    <h2 class="text-xl font-semibold mb-4">{{ __('imageupload::app.admin.image-upload.single_upload') }}</h2>
+                    
+                    <form id="singleUploadForm" enctype="multipart/form-data">
+                        @csrf
+                        <div class="mb-4">
+                            <label for="single_image" class="block text-sm font-medium text-gray-700">
+                                {{ __('imageupload::app.admin.image-upload.select_image') }}
+                            </label>
+                            <input 
+                                type="file" 
+                                id="single_image" 
+                                name="image" 
+                                accept="image/*"
+                                class="mt-1 block w-full"
+                                required
+                            >
+                        </div>
+
+                        <div class="mb-4">
+                            <label for="single_directory" class="block text-sm font-medium text-gray-700">
+                                {{ __('imageupload::app.admin.image-upload.directory') }} ({{ __('imageupload::app.admin.image-upload.optional') }})
+                            </label>
+                            <input 
+                                type="text" 
+                                id="single_directory" 
+                                name="directory" 
+                                placeholder="custom/directory/path"
+                                class="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
+                            >
+                        </div>
+
+                        <button 
+                            type="submit" 
+                            class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
+                        >
+                            {{ __('imageupload::app.admin.image-upload.upload') }}
+                        </button>
+                    </form>
+
+                    <div id="singleUploadResult" class="mt-4"></div>
+                </div>
+
+                <!-- Multiple Image Upload -->
+                <div class="bg-white rounded-lg shadow p-6">
+                    <h2 class="text-xl font-semibold mb-4">{{ __('imageupload::app.admin.image-upload.multiple_upload') }}</h2>
+                    
+                    <form id="multipleUploadForm" enctype="multipart/form-data">
+                        @csrf
+                        <div class="mb-4">
+                            <label for="multiple_images" class="block text-sm font-medium text-gray-700">
+                                {{ __('imageupload::app.admin.image-upload.select_images') }}
+                            </label>
+                            <input 
+                                type="file" 
+                                id="multiple_images" 
+                                name="images[]" 
+                                accept="image/*"
+                                multiple
+                                class="mt-1 block w-full"
+                                required
+                            >
+                        </div>
+
+                        <div class="mb-4">
+                            <label for="multiple_directory" class="block text-sm font-medium text-gray-700">
+                                {{ __('imageupload::app.admin.image-upload.directory') }} ({{ __('imageupload::app.admin.image-upload.optional') }})
+                            </label>
+                            <input 
+                                type="text" 
+                                id="multiple_directory" 
+                                name="directory" 
+                                placeholder="custom/directory/path"
+                                class="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
+                            >
+                        </div>
+
+                        <button 
+                            type="submit" 
+                            class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
+                        >
+                            {{ __('imageupload::app.admin.image-upload.upload_multiple') }}
+                        </button>
+                    </form>
+
+                    <div id="multipleUploadResult" class="mt-4"></div>
+                </div>
+            </div>
+
+            <!-- Uploaded Images Display -->
+            <div class="bg-white rounded-lg shadow p-6 mt-6">
+                <h2 class="text-xl font-semibold mb-4">{{ __('imageupload::app.admin.image-upload.uploaded_images') }}</h2>
+                <div id="uploadedImagesContainer" class="grid grid-cols-4 gap-4">
+                    <p class="text-gray-500 col-span-4">{{ __('imageupload::app.admin.image-upload.no_images_uploaded') }}</p>
+                </div>
+            </div>
+        </div>
+    </div>
+@endsection
+
+@push('scripts')
+<script>
+    // Single image upload
+    document.getElementById('singleUploadForm').addEventListener('submit', async function(e) {
+        e.preventDefault();
+        
+        const formData = new FormData(this);
+        const resultDiv = document.getElementById('singleUploadResult');
+        
+        try {
+            resultDiv.innerHTML = '<p class="text-blue-600">Uploading...</p>';
+            
+            const response = await fetch('{{ route("admin.image_upload.upload") }}', {
+                method: 'POST',
+                body: formData,
+                headers: {
+                    'X-CSRF-TOKEN': '{{ csrf_token() }}'
+                }
+            });
+            
+            const data = await response.json();
+            
+            if (data.success) {
+                resultDiv.innerHTML = `
+                    <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
+                        <p><strong>{{ __('imageupload::app.admin.image-upload.success') }}</strong></p>
+                        <p>URL: <a href="${data.data.url}" target="_blank" class="underline">${data.data.url}</a></p>
+                        <p>Path: ${data.data.path}</p>
+                    </div>
+                `;
+                
+                // Add to uploaded images container
+                addUploadedImage(data.data);
+            } else {
+                resultDiv.innerHTML = `
+                    <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
+                        <p><strong>{{ __('imageupload::app.admin.image-upload.error') }}</strong></p>
+                        <p>${data.message}</p>
+                    </div>
+                `;
+            }
+        } catch (error) {
+            resultDiv.innerHTML = `
+                <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
+                    <p><strong>{{ __('imageupload::app.admin.image-upload.error') }}</strong></p>
+                    <p>${error.message}</p>
+                </div>
+            `;
+        }
+    });
+
+    // Multiple image upload
+    document.getElementById('multipleUploadForm').addEventListener('submit', async function(e) {
+        e.preventDefault();
+        
+        const formData = new FormData(this);
+        const resultDiv = document.getElementById('multipleUploadResult');
+        
+        try {
+            resultDiv.innerHTML = '<p class="text-blue-600">Uploading...</p>';
+            
+            const response = await fetch('{{ route("admin.image_upload.upload_multiple") }}', {
+                method: 'POST',
+                body: formData,
+                headers: {
+                    'X-CSRF-TOKEN': '{{ csrf_token() }}'
+                }
+            });
+            
+            const data = await response.json();
+            
+            if (data.success) {
+                let html = `
+                    <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
+                        <p><strong>{{ __('imageupload::app.admin.image-upload.success') }}</strong></p>
+                        <p>${data.message}</p>
+                    </div>
+                `;
+                
+                data.data.forEach((result, index) => {
+                    if (result.success) {
+                        html += `
+                            <div class="mt-2 p-2 border rounded">
+                                <p><strong>Image ${index + 1}:</strong></p>
+                                <p>URL: <a href="${result.url}" target="_blank" class="underline">${result.url}</a></p>
+                                <p>Path: ${result.path}</p>
+                            </div>
+                        `;
+                        addUploadedImage(result);
+                    } else {
+                        html += `
+                            <div class="mt-2 p-2 border rounded bg-red-50">
+                                <p><strong>Image ${index + 1} Failed:</strong></p>
+                                <p>${result.error}</p>
+                            </div>
+                        `;
+                    }
+                });
+                
+                resultDiv.innerHTML = html;
+            } else {
+                resultDiv.innerHTML = `
+                    <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
+                        <p><strong>{{ __('imageupload::app.admin.image-upload.error') }}</strong></p>
+                        <p>${data.message}</p>
+                    </div>
+                `;
+            }
+        } catch (error) {
+            resultDiv.innerHTML = `
+                <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
+                    <p><strong>{{ __('imageupload::app.admin.image-upload.error') }}</strong></p>
+                    <p>${error.message}</p>
+                </div>
+            `;
+        }
+    });
+
+    // Function to add uploaded image to the display container
+    function addUploadedImage(imageData) {
+        const container = document.getElementById('uploadedImagesContainer');
+        
+        // Remove "no images" message if it exists
+        const noImagesMsg = container.querySelector('.text-gray-500');
+        if (noImagesMsg) {
+            noImagesMsg.remove();
+        }
+        
+        const imageDiv = document.createElement('div');
+        imageDiv.className = 'relative border rounded-lg overflow-hidden';
+        imageDiv.innerHTML = `
+            <img src="${imageData.url}" alt="${imageData.original_name}" class="w-full h-32 object-cover">
+            <div class="p-2">
+                <p class="text-xs truncate">${imageData.original_name}</p>
+                <button onclick="deleteImage('${imageData.path}', '${imageData.disk}')" 
+                        class="text-red-600 text-xs hover:underline">
+                    {{ __('imageupload::app.admin.image-upload.delete') }}
+                </button>
+            </div>
+        `;
+        
+        container.appendChild(imageDiv);
+    }
+
+    // Function to delete an image
+    async function deleteImage(path, disk) {
+        if (!confirm('{{ __('imageupload::app.admin.image-upload.confirm_delete') }}')) {
+            return;
+        }
+        
+        try {
+            const response = await fetch('{{ route("admin.image_upload.delete") }}', {
+                method: 'DELETE',
+                headers: {
+                    'Content-Type': 'application/json',
+                    'X-CSRF-TOKEN': '{{ csrf_token() }}'
+                },
+                body: JSON.stringify({ path, disk })
+            });
+            
+            const data = await response.json();
+            
+            if (data.success) {
+                alert('{{ __('imageupload::app.admin.image-upload.delete_success') }}');
+                location.reload();
+            } else {
+                alert('{{ __('imageupload::app.admin.image-upload.delete_failed') }}: ' + data.message);
+            }
+        } catch (error) {
+            alert('{{ __('imageupload::app.admin.image-upload.delete_failed') }}: ' + error.message);
+        }
+    }
+</script>
+@endpush

+ 520 - 0
packages/Longyi/ImageUpload/src/Resources/views/components/image-upload.blade.php

@@ -0,0 +1,520 @@
+<template>
+    <div class="image-upload-component">
+        <!-- Single Image Upload -->
+        <div v-if="!multiple" class="upload-container">
+            <div 
+                class="upload-area"
+                :class="{ 'drag-over': isDragOver }"
+                @dragover.prevent="onDragOver"
+                @dragleave.prevent="onDragLeave"
+                @drop.prevent="onDrop"
+                @click="triggerFileInput"
+            >
+                <input 
+                    ref="fileInput"
+                    type="file" 
+                    accept="image/*"
+                    @change="handleFileSelect"
+                    style="display: none"
+                >
+                
+                <div v-if="!previewUrl" class="upload-placeholder">
+                    <i class="icon-camera"></i>
+                    <p>{{ label }}</p>
+                    <small>{{ hint }}</small>
+                </div>
+                
+                <div v-else class="image-preview">
+                    <img :src="previewUrl" :alt="fileName">
+                    <button @click.stop="removeImage" class="remove-btn">
+                        <i class="icon-trash"></i>
+                    </button>
+                </div>
+            </div>
+            
+            <div v-if="uploading" class="upload-progress">
+                <div class="progress-bar">
+                    <div class="progress-fill" :style="{ width: progress + '%' }"></div>
+                </div>
+                <p>{{ progress }}%</p>
+            </div>
+            
+            <div v-if="error" class="error-message">
+                {{ error }}
+            </div>
+        </div>
+
+        <!-- Multiple Image Upload -->
+        <div v-else class="upload-container">
+            <div 
+                class="upload-area multiple"
+                :class="{ 'drag-over': isDragOver }"
+                @dragover.prevent="onDragOver"
+                @dragleave.prevent="onDragLeave"
+                @drop.prevent="onDropMultiple"
+                @click="triggerFileInput"
+            >
+                <input 
+                    ref="fileInput"
+                    type="file" 
+                    accept="image/*"
+                    multiple
+                    @change="handleMultipleFileSelect"
+                    style="display: none"
+                >
+                
+                <div class="upload-placeholder">
+                    <i class="icon-camera"></i>
+                    <p>{{ label }}</p>
+                    <small>{{ hint }}</small>
+                </div>
+            </div>
+            
+            <!-- Preview Grid -->
+            <div v-if="previews.length > 0" class="preview-grid">
+                <div v-for="(preview, index) in previews" :key="index" class="preview-item">
+                    <img :src="preview.url" :alt="preview.name">
+                    <button @click="removePreview(index)" class="remove-btn">
+                        <i class="icon-trash"></i>
+                    </button>
+                    <div v-if="preview.uploading" class="uploading-overlay">
+                        <div class="spinner"></div>
+                    </div>
+                </div>
+            </div>
+            
+            <div v-if="uploading" class="upload-progress">
+                <div class="progress-bar">
+                    <div class="progress-fill" :style="{ width: progress + '%' }"></div>
+                </div>
+                <p>{{ progress }}%</p>
+            </div>
+            
+            <div v-if="error" class="error-message">
+                {{ error }}
+            </div>
+            
+            <button 
+                v-if="previews.length > 0 && !uploading"
+                @click="uploadAll"
+                class="upload-btn"
+            >
+                {{ uploadButtonText }}
+            </button>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'ImageUpload',
+    
+    props: {
+        multiple: {
+            type: Boolean,
+            default: false
+        },
+        label: {
+            type: String,
+            default: 'Click or drag to upload'
+        },
+        hint: {
+            type: String,
+            default: 'Supported formats: JPG, PNG, GIF, WebP'
+        },
+        uploadButtonText: {
+            type: String,
+            default: 'Upload Images'
+        },
+        directory: {
+            type: String,
+            default: 'shop/uploads'
+        },
+        maxFiles: {
+            type: Number,
+            default: 10
+        },
+        maxSize: {
+            type: Number,
+            default: 5120 // KB
+        }
+    },
+    
+    data() {
+        return {
+            isDragOver: false,
+            uploading: false,
+            progress: 0,
+            error: null,
+            previewUrl: null,
+            fileName: '',
+            previews: [],
+            selectedFiles: []
+        };
+    },
+    
+    methods: {
+        triggerFileInput() {
+            this.$refs.fileInput.click();
+        },
+        
+        onDragOver() {
+            this.isDragOver = true;
+        },
+        
+        onDragLeave() {
+            this.isDragOver = false;
+        },
+        
+        onDrop(event) {
+            const files = event.dataTransfer.files;
+            if (files.length > 0) {
+                this.handleFile(files[0]);
+            }
+        },
+        
+        onDropMultiple(event) {
+            const files = Array.from(event.dataTransfer.files);
+            this.handleMultipleFiles(files);
+        },
+        
+        handleFileSelect(event) {
+            const file = event.target.files[0];
+            if (file) {
+                this.handleFile(file);
+            }
+        },
+        
+        handleMultipleFileSelect(event) {
+            const files = Array.from(event.target.files);
+            this.handleMultipleFiles(files);
+        },
+        
+        handleFile(file) {
+            // Validate file
+            if (!this.validateFile(file)) {
+                return;
+            }
+            
+            this.fileName = file.name;
+            this.previewUrl = URL.createObjectURL(file);
+            this.selectedFiles = [file];
+            
+            // Auto upload for single file
+            this.uploadSingle(file);
+        },
+        
+        handleMultipleFiles(files) {
+            // Limit number of files
+            if (files.length > this.maxFiles) {
+                this.error = `Maximum ${this.maxFiles} files allowed`;
+                return;
+            }
+            
+            // Validate all files
+            const validFiles = files.filter(file => this.validateFile(file));
+            
+            if (validFiles.length === 0) {
+                return;
+            }
+            
+            // Create previews
+            validFiles.forEach(file => {
+                this.previews.push({
+                    file: file,
+                    url: URL.createObjectURL(file),
+                    name: file.name,
+                    uploading: false,
+                    uploaded: false
+                });
+            });
+            
+            this.selectedFiles = validFiles;
+        },
+        
+        validateFile(file) {
+            // Check file type
+            if (!file.type.startsWith('image/')) {
+                this.error = 'Only image files are allowed';
+                return false;
+            }
+            
+            // Check file size
+            const maxSizeBytes = this.maxSize * 1024;
+            if (file.size > maxSizeBytes) {
+                this.error = `File size must be less than ${this.maxSize}KB`;
+                return false;
+            }
+            
+            this.error = null;
+            return true;
+        },
+        
+        removeImage() {
+            this.previewUrl = null;
+            this.fileName = '';
+            this.selectedFiles = [];
+            this.$refs.fileInput.value = '';
+            this.$emit('remove');
+        },
+        
+        removePreview(index) {
+            this.previews.splice(index, 1);
+            this.selectedFiles.splice(index, 1);
+        },
+        
+        async uploadSingle(file) {
+            this.uploading = true;
+            this.progress = 0;
+            this.error = null;
+            
+            const formData = new FormData();
+            formData.append('image', file);
+            formData.append('directory', this.directory);
+            
+            try {
+                const response = await fetch('/api/image-upload/upload', {
+                    method: 'POST',
+                    body: formData,
+                    headers: {
+                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content
+                    }
+                });
+                
+                const data = await response.json();
+                
+                if (data.success) {
+                    this.progress = 100;
+                    this.$emit('uploaded', data.data);
+                } else {
+                    this.error = data.message;
+                }
+            } catch (err) {
+                this.error = 'Upload failed. Please try again.';
+            } finally {
+                this.uploading = false;
+            }
+        },
+        
+        async uploadAll() {
+            if (this.previews.length === 0) return;
+            
+            this.uploading = true;
+            this.error = null;
+            
+            const formData = new FormData();
+            this.selectedFiles.forEach((file, index) => {
+                formData.append('images[]', file);
+                this.previews[index].uploading = true;
+            });
+            formData.append('directory', this.directory);
+            
+            try {
+                const response = await fetch('/api/image-upload/upload-multiple', {
+                    method: 'POST',
+                    body: formData,
+                    headers: {
+                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content
+                    }
+                });
+                
+                const data = await response.json();
+                
+                if (data.success) {
+                    this.progress = 100;
+                    this.previews.forEach(preview => {
+                        preview.uploading = false;
+                        preview.uploaded = true;
+                    });
+                    this.$emit('uploaded', data.data);
+                } else {
+                    this.error = data.message;
+                }
+            } catch (err) {
+                this.error = 'Upload failed. Please try again.';
+            } finally {
+                this.uploading = false;
+            }
+        }
+    }
+};
+</script>
+
+<style scoped>
+.image-upload-component {
+    width: 100%;
+}
+
+.upload-container {
+    margin-bottom: 20px;
+}
+
+.upload-area {
+    border: 2px dashed #ccc;
+    border-radius: 8px;
+    padding: 40px;
+    text-align: center;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    background: #f9f9f9;
+}
+
+.upload-area:hover,
+.upload-area.drag-over {
+    border-color: #4CAF50;
+    background: #f0f8f0;
+}
+
+.upload-area.multiple {
+    min-height: 200px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.upload-placeholder i {
+    font-size: 48px;
+    color: #999;
+    margin-bottom: 10px;
+}
+
+.upload-placeholder p {
+    margin: 10px 0;
+    color: #333;
+    font-size: 16px;
+}
+
+.upload-placeholder small {
+    color: #999;
+    font-size: 12px;
+}
+
+.image-preview {
+    position: relative;
+    display: inline-block;
+}
+
+.image-preview img {
+    max-width: 300px;
+    max-height: 300px;
+    border-radius: 8px;
+}
+
+.preview-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+    gap: 15px;
+    margin-top: 20px;
+}
+
+.preview-item {
+    position: relative;
+    aspect-ratio: 1;
+    border-radius: 8px;
+    overflow: hidden;
+    border: 1px solid #ddd;
+}
+
+.preview-item img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+}
+
+.remove-btn {
+    position: absolute;
+    top: 5px;
+    right: 5px;
+    background: rgba(255, 0, 0, 0.8);
+    color: white;
+    border: none;
+    border-radius: 50%;
+    width: 30px;
+    height: 30px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: background 0.3s;
+}
+
+.remove-btn:hover {
+    background: rgba(255, 0, 0, 1);
+}
+
+.uploading-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.spinner {
+    width: 40px;
+    height: 40px;
+    border: 4px solid #f3f3f3;
+    border-top: 4px solid #4CAF50;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+.upload-progress {
+    margin-top: 15px;
+}
+
+.progress-bar {
+    width: 100%;
+    height: 8px;
+    background: #e0e0e0;
+    border-radius: 4px;
+    overflow: hidden;
+}
+
+.progress-fill {
+    height: 100%;
+    background: #4CAF50;
+    transition: width 0.3s ease;
+}
+
+.upload-progress p {
+    text-align: center;
+    margin-top: 5px;
+    color: #666;
+    font-size: 14px;
+}
+
+.error-message {
+    margin-top: 10px;
+    padding: 10px;
+    background: #ffebee;
+    color: #c62828;
+    border-radius: 4px;
+    font-size: 14px;
+}
+
+.upload-btn {
+    margin-top: 15px;
+    padding: 12px 24px;
+    background: #4CAF50;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 16px;
+    transition: background 0.3s;
+}
+
+.upload-btn:hover {
+    background: #45a049;
+}
+</style>

+ 13 - 0
packages/Longyi/ImageUpload/src/Routes/admin-routes.php

@@ -0,0 +1,13 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\ImageUpload\Http\Controllers\Admin\ImageUploadController;
+
+Route::group(['middleware' => ['web', 'admin'], 'prefix' => config('app.admin_path')], function () {
+    Route::controller(ImageUploadController::class)->prefix('image-upload')->group(function () {
+        Route::post('upload', 'upload')->name('admin.image_upload.upload');
+        Route::post('upload-multiple', 'uploadMultiple')->name('admin.image_upload.upload_multiple');
+        Route::delete('delete', 'delete')->name('admin.image_upload.delete');
+        Route::get('url', 'getUrl')->name('admin.image_upload.get_url');
+    });
+});

+ 20 - 0
packages/Longyi/ImageUpload/src/Routes/shop-routes.php

@@ -0,0 +1,20 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\ImageUpload\Http\Controllers\Shop\ImageUploadController;
+
+// Shop/Frontend routes - accessible to all users
+Route::group(['middleware' => ['web'], 'prefix' => 'api'], function () {
+    Route::controller(ImageUploadController::class)->prefix('image-upload')->group(function () {
+        Route::post('upload', 'upload')->name('shop.image_upload.upload');
+        Route::post('upload-multiple', 'uploadMultiple')->name('shop.image_upload.upload_multiple');
+        Route::delete('delete', 'delete')->name('shop.image_upload.delete');
+        Route::get('url', 'getUrl')->name('shop.image_upload.get_url');
+    });
+});
+
+// Optional: Protected routes for authenticated customers only
+Route::group(['middleware' => ['web', 'customer'], 'prefix' => 'api'], function () {
+    // Add routes that require customer authentication here if needed
+    // For example, customer profile image upload
+});

+ 200 - 0
packages/Longyi/ImageUpload/src/Services/ImageUploadService.php

@@ -0,0 +1,200 @@
+<?php
+
+namespace Longyi\ImageUpload\Services;
+
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use Intervention\Image\ImageManager;
+use Exception;
+use Aws\S3\S3Client;
+use Aws\Exception\AwsException;
+class ImageUploadService
+{
+    /**
+     * Upload an image to the configured storage disk.
+     *
+     * @param UploadedFile $file
+     * @param string|null $directory Custom directory path
+     * @return array
+     * @throws Exception
+     */
+    public function upload(UploadedFile $file, ?string $directory = null): array
+    {
+        // Validate the file
+        $this->validateFile($file);
+
+        // Determine the disk to use
+        $disk = config('image_upload.default_disk', 's3');
+
+        // Generate directory path
+        $uploadDirectory = $directory ?? config('image_upload.upload_directory', 'images/uploads');
+        $path = $uploadDirectory . '/' . date('Ym/d');
+
+        // Process and store the image
+        if (Str::contains($file->getMimeType(), 'image')) {
+            return $this->processAndStoreImage($file, $path, $disk);
+        } else {
+            return $this->storeFile($file, $path, $disk);
+        }
+    }
+
+    /**
+     * Upload multiple images.
+     *
+     * @param array $files
+     * @param string|null $directory
+     * @return array
+     */
+    public function uploadMultiple(array $files, ?string $directory = null): array
+    {
+        $results = [];
+
+        foreach ($files as $file) {
+            if ($file instanceof UploadedFile) {
+                try {
+                    $results[] = $this->upload($file, $directory);
+                } catch (Exception $e) {
+                    $results[] = [
+                        'success' => false,
+                        'error' => $e->getMessage(),
+                        'original_name' => $file->getClientOriginalName(),
+                    ];
+                }
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Validate the uploaded file.
+     *
+     * @param UploadedFile $file
+     * @throws Exception
+     */
+    protected function validateFile(UploadedFile $file): void
+    {
+        // Check file size
+        $maxSize = config('image_upload.max_size', 5120) * 1024; // Convert KB to bytes
+        if ($file->getSize() > $maxSize) {
+            throw new Exception(__('The file size exceeds the maximum allowed size of :size KB.', [
+                'size' => config('image_upload.max_size', 5120)
+            ]));
+        }
+
+        // Check file type
+        $allowedTypes = config('image_upload.allowed_types', []);
+        if (!in_array($file->getMimeType(), $allowedTypes)) {
+            throw new Exception(__('The file type is not allowed. Allowed types: :types', [
+                'types' => implode(', ', $allowedTypes)
+            ]));
+        }
+    }
+
+    /**
+     * Process and store an image file.
+     *
+     * @param UploadedFile $file
+     * @param string $path
+     * @param string $disk
+     * @return array
+     */
+    protected function processAndStoreImage(UploadedFile $file, string $path, string $disk): array
+    {
+
+            $manager = new ImageManager();
+            $image = $manager->make($file)->encode('webp');
+
+            $fileName = Str::random(40) . '.webp';
+            $fullPath = $path . '/' . $fileName;
+
+            // 方式一:使用原生 AWS SDK 绕过 Laravel Storage
+            $s3Client = new S3Client([
+                'version' => 'latest',
+                'region'  => env('AWS_DEFAULT_REGION'),
+                'credentials' => [
+                    'key'    => env('AWS_ACCESS_KEY_ID'),
+                    'secret' => env('AWS_SECRET_ACCESS_KEY'),
+                ],
+                'http'    => [
+                    'verify' => false,  // 临时禁用 SSL 验证用于测试
+                ],
+            ]);
+
+            $result = $s3Client->putObject([
+                'Bucket' => env('AWS_BUCKET'),
+                'Key'    => $fullPath,
+                'Body'   => (string) $image,
+                'ContentType' => 'image/webp',
+            ]);
+            return [
+                'success' => true,
+                'path' => $fullPath,
+                'url' => Storage::disk($disk)->url($fullPath),
+                'original_name' => $file->getClientOriginalName(),
+                'mime_type' => $file->getMimeType(),
+                'size' => $file->getSize(),
+                'disk' => $disk,
+            ];
+
+    }
+
+    /**
+     * Store a non-image file.
+     *
+     * @param UploadedFile $file
+     * @param string $path
+     * @param string $disk
+     * @return array
+     */
+    protected function storeFile(UploadedFile $file, string $path, string $disk): array
+    {
+        try {
+            $fileName = Str::random(40) . '.' . $file->getClientOriginalExtension();
+            $fullPath = $path . '/' . $fileName;
+
+            Storage::disk($disk)->putFileAs($path, $file, $fileName);
+
+            return [
+                'success' => true,
+                'path' => $fullPath,
+                'url' => Storage::disk($disk)->url($fullPath),
+                'original_name' => $file->getClientOriginalName(),
+                'mime_type' => $file->getMimeType(),
+                'size' => $file->getSize(),
+                'disk' => $disk,
+            ];
+        } catch (Exception $e) {
+            throw new Exception(__('Failed to store file: :error', ['error' => $e->getMessage()]));
+        }
+    }
+
+    /**
+     * Delete an image from storage.
+     *
+     * @param string $path
+     * @param string|null $disk
+     * @return bool
+     */
+    public function delete(string $path, ?string $disk = null): bool
+    {
+        $disk = $disk ?? config('image_upload.default_disk', 's3');
+
+        return Storage::disk($disk)->delete($path);
+    }
+
+    /**
+     * Get the URL for a stored image.
+     *
+     * @param string $path
+     * @param string|null $disk
+     * @return string
+     */
+    public function getUrl(string $path, ?string $disk = null): string
+    {
+        $disk = $disk ?? config('image_upload.default_disk', 's3');
+
+        return Storage::disk($disk)->url($path);
+    }
+}

+ 23 - 0
packages/Longyi/Member/package.json

@@ -0,0 +1,23 @@
+{
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build"
+  },
+  "devDependencies": {
+    "autoprefixer": "^10.4.14",
+    "axios": "^1.4.0",
+    "laravel-vite-plugin": "^0.7.2",
+    "postcss": "^8.4.23",
+    "tailwindcss": "^3.3.2",
+    "vite": "^4.0.0",
+    "vue": "^3.2.47"
+  },
+  "dependencies": {
+    "@vee-validate/i18n": "^4.9.1",
+    "@vee-validate/rules": "^4.9.1",
+    "@vitejs/plugin-vue": "^4.2.3",
+    "mitt": "^3.0.1",
+    "vee-validate": "^4.9.1"
+  }
+}

+ 6 - 0
packages/Longyi/Member/postcss.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+}

+ 10 - 0
packages/Longyi/Member/src/Config/acl.php

@@ -0,0 +1,10 @@
+<?php
+
+return [
+    [
+        'key'   => 'member',
+        'name'  => 'Member',
+        'route' => 'admin.member.index',
+        'sort'  => 101
+    ]
+];

+ 11 - 0
packages/Longyi/Member/src/Config/admin-menu.php

@@ -0,0 +1,11 @@
+<?php
+
+return [
+    [
+        'key'   => 'member',
+        'name'  => 'Member',
+        'route' => 'admin.member.index',
+        'sort'  => 101,
+        'icon'  => 'icon-sales',
+    ]
+];

+ 7 - 0
packages/Longyi/Member/src/Contracts/MemberLog.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Longyi\Member\Contracts;
+
+interface MemberLog
+{
+}

+ 107 - 0
packages/Longyi/Member/src/DataGrids/Member/MemberDataGrid.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace Longyi\Member\DataGrids\Member;
+
+use Illuminate\Support\Facades\DB;
+use Webkul\DataGrid\DataGrid;
+
+class MemberDataGrid extends DataGrid
+{
+    /**
+     * Prepare query builder.
+     *
+     * @return \Illuminate\Database\Query\Builder
+     */
+    public function prepareQueryBuilder()
+    {
+        $queryBuilder = DB::table('member_log')
+            ->leftJoin('customers', 'member_log.customer_id', '=', 'customers.id')
+            ->select(
+                'member_log.id',
+                'member_log.customer_id',
+                'member_log.order_id',
+                'member_log.amount',
+                'customers.email as customer_email',
+                'customers.first_name',
+                'customers.last_name',
+                'member_log.expirationdate',
+                'member_log.created_at',
+                'member_log.updated_at'
+            );
+
+        $this->addFilter('id', 'member_log.id');
+        $this->addFilter('order_id', 'member_log.order_id');
+        $this->addFilter('amount', 'member_log.amount');
+        $this->addFilter('expirationdate', 'member_log.expirationdate');
+        $this->addFilter('created_at', 'member_log.created_at');
+        $this->addFilter('customer_email', 'customers.email');
+
+        return $queryBuilder;
+    }
+
+    /**
+     * Add columns.
+     *
+     * @return void
+     */
+    public function prepareColumns()
+    {
+        $this->addColumn([
+            'index'      => 'id',
+            'label'      => 'id',
+            'type'       => 'integer',
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+        $this->addColumn([
+            'index'      => 'customer_email',
+            'label'      => '用户邮箱',
+            'type'       => 'string',
+            'filterable' => true,
+            'sortable'   => true,
+            'searchable' => true,
+            'closure'    => function ($row) {
+                if ($row->customer_email) {
+                    $customerName = '';
+                    if ($row->first_name || $row->last_name) {
+                        $customerName = ' (' . trim($row->first_name . ' ' . $row->last_name) . ')';
+                    }
+                    return $row->customer_email . $customerName;
+                }
+            },
+        ]);
+        $this->addColumn([
+            'index'      => 'order_id',
+            'label'      => '#order Id',
+            'type'       => 'integer',
+            'filterable' => false,
+            'sortable'   => true
+        ]);
+        $this->addColumn([
+            'index'      => 'amount',
+            'label'      => 'vip amount',
+            'type'       => 'aggregate',
+            'filterable' => false,
+            'sortable'   => true,
+            'closure'    => function ($row) {
+                return core()->formatPrice($row->amount);
+            },
+        ]);
+        $this->addColumn([
+            'index'      => 'expirationdate',
+            'label'      => 'expirationdate',
+            'type'       => 'date',
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+
+
+        $this->addColumn([
+            'index'      => 'created_at',
+            'label'      => 'created at',
+            'type'       => 'datetime',
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+    }
+}

+ 28 - 0
packages/Longyi/Member/src/Database/Migrations/2026_04_15_233520_add_vip_expire_fields_to_customer_table.php

@@ -0,0 +1,28 @@
+<?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('customers', function (Blueprint $table) {
+            $table->dateTime('vip_expire_date')->nullable()->after('remember_token');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('customers', function (Blueprint $table) {
+            $table->dropColumn('vip_expire_date');
+        });
+    }
+};

+ 31 - 0
packages/Longyi/Member/src/Database/Migrations/2026_04_15_233955_add_member_fields_to_cart_table.php

@@ -0,0 +1,31 @@
+<?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('cart', function (Blueprint $table) {
+            $table->decimal('vip_plus_amount', 10, 2)->nullable()->after('base_giftcard_amount');
+            $table->decimal('base_vip_plus_amount', 10, 2)->nullable()->after('vip_plus_amount');
+            $table->decimal('vip_discount_amount', 10, 2)->nullable()->after('base_vip_plus_amount');
+            $table->decimal('base_vip_discount_amount', 10, 2)->nullable()->after('vip_discount_amount');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            $table->dropColumn(['vip_plus_amount','base_vip_plus_amount', 'vip_discount_amount', 'vip_discount_amount']);
+        });
+    }
+};

+ 31 - 0
packages/Longyi/Member/src/Database/Migrations/2026_04_15_234016_add_member_fields_to_orders_table.php

@@ -0,0 +1,31 @@
+<?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('orders', function (Blueprint $table) {
+            $table->decimal('vip_plus_amount', 10, 2)->nullable()->after('base_giftcard_amount');
+            $table->decimal('base_vip_plus_amount', 10, 2)->nullable()->after('vip_plus_amount');
+            $table->decimal('vip_discount_amount', 10, 2)->nullable()->after('base_vip_plus_amount');
+            $table->decimal('base_vip_discount_amount', 10, 2)->nullable()->after('vip_discount_amount');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('orders', function (Blueprint $table) {
+            $table->dropColumn(['vip_plus_amount','base_vip_plus_amount', 'vip_discount_amount', 'vip_discount_amount']);
+        });
+    }
+};

+ 31 - 0
packages/Longyi/Member/src/Database/Migrations/2026_04_15_234035_add_member_fields_to_invoices_table.php

@@ -0,0 +1,31 @@
+<?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('invoices', function (Blueprint $table) {
+            $table->decimal('vip_plus_amount', 10, 2)->nullable()->after('base_giftcard_amount');
+            $table->decimal('base_vip_plus_amount', 10, 2)->nullable()->after('vip_plus_amount');
+            $table->decimal('vip_discount_amount', 10, 2)->nullable()->after('base_vip_plus_amount');
+            $table->decimal('base_vip_discount_amount', 10, 2)->nullable()->after('vip_discount_amount');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('invoices', function (Blueprint $table) {
+            $table->dropColumn(['vip_plus_amount','base_vip_plus_amount', 'vip_discount_amount', 'vip_discount_amount']);
+        });
+    }
+};

+ 32 - 0
packages/Longyi/Member/src/Database/Migrations/2026_04_20_180518_create_member_log_table.php

@@ -0,0 +1,32 @@
+<?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::create('member_log', function (Blueprint $table) {
+            $table->id();
+            $table->integer('customer_id')->index()->default(0)->nullable();
+            $table->integer('order_id')->index()->default(0)->nullable();
+            $table->tinyInteger('type')->default(1)->nullable();
+            $table->decimal('amount', 10, 2)->nullable()->comment('amount');
+            $table->dateTime('expirationdate')->nullable()->comment('过期时间');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('member_log');
+    }
+};

+ 23 - 0
packages/Longyi/Member/src/Http/Controllers/Admin/MemberController.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Longyi\Member\Http\Controllers\Admin;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\View\View;
+use Longyi\Member\DataGrids\Member\MemberDataGrid;
+use Webkul\Admin\Http\Controllers\Controller;
+
+class MemberController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     */
+    public function index(): View|JsonResponse
+    {
+        if (request()->ajax()) {
+            return datagrid(MemberDataGrid::class)->process();
+        }
+
+        return view('member::admin.index');
+    }
+}

+ 17 - 0
packages/Longyi/Member/src/Http/Controllers/Shop/MemberController.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Longyi\Member\Http\Controllers\Shop;
+
+use Illuminate\View\View;
+use Webkul\Shop\Http\Controllers\Controller;
+
+class MemberController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     */
+    public function index(): View
+    {
+        return view('member::shop.index');
+    }
+}

+ 99 - 0
packages/Longyi/Member/src/Http/Controllers/Shop/MemberDiscountController.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace Longyi\Member\Http\Controllers\Shop;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Response;
+use Webkul\Checkout\Facades\Cart;
+use Webkul\Shop\Http\Controllers\Controller;
+
+class MemberDiscountController extends Controller
+{
+    protected const MEMBER_DISCOUNT_AMOUNT = 19.9;
+
+    /**
+     * Apply member discount to cart.
+     */
+    public function apply(): JsonResponse
+    {
+        try {
+            if (!auth()->guard('customer')->check()) {
+                return response()->json([
+                    'success' => false,
+                    'message' => 'Please log in to your member account first',
+                ], Response::HTTP_UNAUTHORIZED);
+            }
+
+            $cart = Cart::getCart();
+
+            if (!$cart) {
+                return response()->json([
+                    'success' => false,
+                    'message' => 'Shopping cart is empty',
+                ], Response::HTTP_NOT_FOUND);
+            }
+
+            if ($cart->vip_plus_amount && $cart->vip_plus_amount > 0) {
+                return response()->json([
+                    'success' => false,
+                    'message' => 'Member discount has been applied',
+                ], Response::HTTP_OK);
+            }
+
+            $discountAmountInCurrentCurrency = core()->convertPrice(self::MEMBER_DISCOUNT_AMOUNT);
+
+            $cart->vip_plus_amount = $discountAmountInCurrentCurrency;
+            $cart->base_vip_plus_amount = core()->convertToBasePrice($discountAmountInCurrentCurrency);
+            $cart->save();
+            Cart::collectTotals();
+
+            return response()->json([
+                'success' => true,
+                'message' => 'Member discount has been applied',
+                'discount_amount' => $cart->vip_plus_amount,
+            ]);
+        } catch (\Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Failed to apply member discount',
+                'error' => $e->getMessage(),
+            ], Response::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Remove member discount from cart.
+     */
+    public function remove(): JsonResponse
+    {
+        try {
+            $cart = Cart::getCart();
+
+            if (!$cart || !$cart->vip_plus_amount || $cart->vip_plus_amount <= 0) {
+                return response()->json([
+                    'success' => false,
+                    'message' => 'Member discount not applied',
+                ], Response::HTTP_OK);
+            }
+
+            $discountAmount = $cart->vip_plus_amount;
+            $baseDiscountAmount = $cart->base_vip_plus_amount;
+
+            $cart->vip_plus_amount = null;
+            $cart->base_vip_plus_amount = null;
+            $cart->save();
+            Cart::collectTotals();
+
+            return response()->json([
+                'success' => true,
+                'message' => 'Member discount has been removed',
+            ]);
+        } catch (\Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Failed to remove member discount',
+                'error' => $e->getMessage(),
+            ], Response::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+}

+ 47 - 0
packages/Longyi/Member/src/Listeners/MemberHandler.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace Longyi\Member\Listeners;
+
+use Webkul\Paypal\Payment\SmartButton;
+use Webkul\Sales\Repositories\OrderTransactionRepository;
+
+class MemberHandler
+{
+    protected const MEMBER_DISCOUNT_AMOUNT = 19.9;
+    /**
+     * Create a new listener instance.
+     *
+     * @return void
+     */
+    public function __construct(
+        protected SmartButton $smartButton,
+        protected OrderTransactionRepository $orderTransactionRepository
+    ) {}
+
+    /**
+     * Apply member discount to cart.
+     *
+     * @param  \Webkul\Checkout\Models\Cart  $cart
+     * @return void
+     */
+    public function applyMemberDiscount($cart)
+    {
+
+        if (!$cart || !auth()->guard('customer')->check()) {
+            return;
+        }
+        if ($cart->vip_plus_amount <= 0) {
+            return;
+        }
+        $discountAmountInCurrentCurrency = core()->convertPrice(self::MEMBER_DISCOUNT_AMOUNT);
+
+        $cart->vip_plus_amount = $discountAmountInCurrentCurrency;
+        $cart->base_vip_plus_amount = core()->convertToBasePrice($discountAmountInCurrentCurrency);
+
+        $cart->grand_total = max(0, round($cart->grand_total + $discountAmountInCurrentCurrency, 2));
+        $cart->base_grand_total = max(0, round($cart->base_grand_total + $cart->base_vip_plus_amount, 2));
+
+        $cart->save();
+    }
+
+}

+ 63 - 0
packages/Longyi/Member/src/Listeners/OrderPlacedHandler.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace Longyi\Member\Listeners;
+
+use Longyi\Member\Models\MemberLog;
+use Webkul\Paypal\Payment\SmartButton;
+use Webkul\Sales\Repositories\OrderTransactionRepository;
+use Webkul\Customer\Models\Customer;
+use Carbon\Carbon;
+
+class OrderPlacedHandler
+{
+    protected const MEMBER_DISCOUNT_AMOUNT = 19.9;
+
+
+    protected const VIP_EXTENSION_DAYS = 180;
+    /**
+     * Create a new listener instance.
+     *
+     * @return void
+     */
+    public function __construct(
+        protected SmartButton $smartButton,
+        protected OrderTransactionRepository $orderTransactionRepository
+    ) {}
+
+    /**
+     * Apply member discount to cart.
+     *
+     * @param  \Webkul\Checkout\Models\Cart  $cart
+     * @return void
+     */
+    public function afterPlaceOrder($order)
+    {
+        if (!$order || !$order->customer_id) {
+            return;
+        }
+        if (!$order->vip_plus_amount || $order->vip_plus_amount <= 0) {
+            return;
+        }
+        $customer = Customer::find($order->customer_id);
+        if (!$customer) {
+            return;
+        }
+        $currentExpireDate = $customer->vip_expire_date ? Carbon::parse($customer->vip_expire_date) : now();
+        if ($currentExpireDate->isPast()) {
+            $newExpireDate = now()->addDays(self::VIP_EXTENSION_DAYS);
+        } else {
+            $newExpireDate = $currentExpireDate->addDays(self::VIP_EXTENSION_DAYS);
+        }
+        $customer->vip_expire_date = $newExpireDate;
+        $customer->save();
+        // 记录
+        MemberLog::create([
+            'customer_id' => $order->customer_id,
+            'order_id' => $order->increment_id,
+            'amount' => self::MEMBER_DISCOUNT_AMOUNT,
+            'type' => 1,
+            'expirationdate' => $newExpireDate
+        ]);
+    }
+
+}

+ 28 - 0
packages/Longyi/Member/src/Listeners/OrderViewHandler.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Longyi\Member\Listeners;
+
+use Webkul\Sales\Models\Order;
+
+class OrderViewHandler
+{
+    /**
+     * Handle order view information discount before event.
+     *
+     * @param  array  $data
+     * @return void
+     */
+    public function renderMemberInfo($viewRenderEventManager)
+    {
+        // 从当前路由参数中获取订单ID
+        $orderId = request()->route('id');
+
+        if (!$orderId) {
+            return;
+        }
+
+        // 查询订单
+        $order = Order::find($orderId);
+        echo view('member::customers.account.orders.member-info', compact('order'))->render();
+    }
+}

+ 98 - 0
packages/Longyi/Member/src/Listeners/VipDiscountHandler.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace Longyi\Member\Listeners;
+
+use Carbon\Carbon;
+
+class VipDiscountHandler
+{
+    protected const VIP_DISCOUNT_PERCENTAGE = 0.05; // 5%
+
+    /**
+     * Apply VIP discount to cart.
+     *
+     * @param  \Webkul\Checkout\Models\Cart  $cart
+     * @return void
+     */
+    public function applyVipDiscount($cart)
+    {
+        if (!$cart || !auth()->guard('customer')->check()) {
+            return;
+        }
+        $isVip = false;
+        $customer = auth()->guard('customer')->user();
+        if ($cart->vip_plus_amount > 0 ||  $this->isValidVip($customer)) {
+            $isVip = true;
+        }
+        // 检查是否是有效 VIP
+        if (!$isVip) {
+            // 如果不是有效 VIP,清除可能存在的 VIP 折扣
+            if ($cart->vip_discount_amount && $cart->vip_discount_amount > 0) {
+                $this->removeVipDiscount($cart);
+            }
+            return;
+        }
+
+        // 计算购物车小计
+        $subTotal = $cart->sub_total;
+
+        if ($subTotal <= 0) {
+            return;
+        }
+
+        // 计算 5% 折扣
+        $discountAmountInCurrentCurrency = round($subTotal * self::VIP_DISCOUNT_PERCENTAGE, 2);
+        $baseDiscountAmount = core()->convertToBasePrice($discountAmountInCurrentCurrency);
+        // 应用新的 VIP 折扣
+        $cart->vip_discount_amount = $discountAmountInCurrentCurrency;
+        $cart->base_vip_discount_amount = $baseDiscountAmount;
+
+        $cart->grand_total = max(0, round($cart->grand_total - $discountAmountInCurrentCurrency, 2));
+        $cart->base_grand_total = max(0, round($cart->base_grand_total - $baseDiscountAmount, 2));
+
+        $cart->save();
+    }
+
+    /**
+     * Remove VIP discount from cart.
+     *
+     * @param  \Webkul\Checkout\Models\Cart  $cart
+     * @return void
+     */
+    public function removeVipDiscount($cart)
+    {
+        if (!$cart || !$cart->vip_discount_amount || $cart->vip_discount_amount <= 0) {
+            return;
+        }
+
+        // 1. 先取出要恢复的金额(在清空之前!)
+        $discountAmount = $cart->vip_discount_amount;
+        $baseDiscountAmount = $cart->base_vip_discount_amount;
+
+        // 2. 再清空字段
+        $cart->vip_discount_amount = null;
+        $cart->base_vip_discount_amount = null;
+
+        // 3. 恢复购物车总额
+        $cart->grand_total = round($cart->grand_total, 2);
+        $cart->base_grand_total = round($cart->base_grand_total, 2);
+
+        $cart->save();
+    }
+
+    /**
+     * Check if customer is a valid VIP.
+     *
+     * @param  \Webkul\Customer\Models\Customer  $customer
+     * @return bool
+     */
+    protected function isValidVip($customer): bool
+    {
+        if (!$customer->vip_expire_date) {
+            return false;
+        }
+
+        $expireDate = Carbon::parse($customer->vip_expire_date);
+        return !$expireDate->isPast();
+    }
+}

+ 39 - 0
packages/Longyi/Member/src/Models/MemberLog.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace Longyi\Member\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Longyi\Member\Contracts\MemberLog as MemberLogContract;
+
+class MemberLog extends Model implements MemberLogContract
+{
+    use HasFactory;
+
+    protected $table = 'member_log';
+    /**
+     * The attributes that are mass assignable.
+     *
+     * @var array
+     */
+    protected $fillable = [
+        'customer_id',
+        'order_id',
+        'type',
+        'amount',
+        'expirationdate'
+    ];
+
+    /**
+     * The attributes that should be cast.
+     *
+     * @var array
+     */
+    protected $casts = [
+        'customer_id' => 'integer',
+        'order_id' => 'integer',
+        'type' => 'integer',
+        'amount' => 'decimal:2',
+        'expirationdate' => 'date',
+    ];
+}

+ 9 - 0
packages/Longyi/Member/src/Models/MemberLogProxy.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace Longyi\Member\Models;
+
+use Konekt\Concord\Proxies\ModelProxy;
+
+class MemberLogProxy extends ModelProxy
+{
+}

+ 33 - 0
packages/Longyi/Member/src/Providers/EventServiceProvider.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Longyi\Member\Providers;
+
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\ServiceProvider;
+use Longyi\Member\Listeners\MemberHandler;
+use Longyi\Member\Listeners\VipDiscountHandler;
+
+class EventServiceProvider extends ServiceProvider
+{
+    /**
+     * Bootstrap services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        Event::listen('checkout.cart.collect.totals.after', 'Longyi\Member\Listeners\MemberHandler@applyMemberDiscount');
+        Event::listen('checkout.cart.collect.totals.after', 'Longyi\Member\Listeners\VipDiscountHandler@applyVipDiscount');
+        Event::listen('bagisto.shop.checkout.onepage.summary.coupon.after', function($viewRenderEventManager) {
+            $viewRenderEventManager->addTemplate('member::components.vip-member-discount-cartsummary');
+        });
+        Event::listen('bagisto.shop.checkout.cart.summary.vip_member_discount.after', function($viewRenderEventManager) {
+            $viewRenderEventManager->addTemplate('member::components.member-discount-cartsummary');
+        });
+        Event::listen('bagisto.admin.sales.order.view.giftcard.before', function($viewRenderEventManager) {
+            $viewRenderEventManager->addTemplate('member::sales.orders.view');
+        });
+        Event::listen('checkout.order.save.after', 'Longyi\Member\Listeners\OrderPlacedHandler@afterPlaceOrder');
+        Event::listen('bagisto.shop.customers.account.orders.view.information.discount.after', 'Longyi\Member\Listeners\OrderViewHandler@renderMemberInfo');
+    }
+}

+ 55 - 0
packages/Longyi/Member/src/Providers/MemberServiceProvider.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace Longyi\Member\Providers;
+
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\Event;
+use Longyi\Member\Providers\EventServiceProvider;
+
+class MemberServiceProvider extends ServiceProvider
+{
+    /**
+     * Register services.
+     */
+    public function register(): void
+    {
+        $this->registerConfig();
+    }
+
+    /**
+     * Bootstrap services.
+     */
+    public function boot(): void
+    {
+        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
+
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
+
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/shop-routes.php');
+
+        $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'member');
+
+        $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'member');
+
+        Event::listen('bagisto.admin.layout.head', function($viewRenderEventManager) {
+            $viewRenderEventManager->addTemplate('member::admin.layouts.style');
+        });
+        $this->app->register(EventServiceProvider::class);
+    }
+
+    /**
+     * Register package config.
+     *
+     * @return void
+     */
+    protected function registerConfig()
+    {
+        $this->mergeConfigFrom(
+            dirname(__DIR__) . '/Config/admin-menu.php', 'menu.admin'
+        );
+
+        $this->mergeConfigFrom(
+            dirname(__DIR__) . '/Config/acl.php', 'acl'
+        );
+    }
+}

+ 15 - 0
packages/Longyi/Member/src/Providers/ModuleServiceProvider.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace Longyi\Member\Providers;
+
+use Konekt\Concord\BaseModuleServiceProvider;
+
+class ModuleServiceProvider extends BaseModuleServiceProvider
+{
+    /**
+     * Models.
+     *
+     * @var array
+     */
+    protected $models = [];
+}

+ 16 - 0
packages/Longyi/Member/src/Repositories/MemberLogRepository.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Longyi\Member\Repositories;
+
+use Webkul\Core\Eloquent\Repository;
+
+class MemberLogRepository extends Repository
+{
+    /**
+     * Specify model class name.
+     */
+    public function model(): string
+    {
+        return 'Longyi\Member\Contracts\MemberLog';
+    }
+}

+ 23 - 0
packages/Longyi/Member/src/Resources/assets/css/app.css

@@ -0,0 +1,23 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer components {
+    .temp-icon {
+        width: 48px;
+        height: 48px;
+        display: inline-block;
+        background-size: cover;
+        background-image: url("../images/icon-temp.svg");
+    }
+
+    .active {
+        .temp-icon {
+            background-image: url("../images/icon-temp-active.svg");
+        }
+
+        &.temp-icon {
+            background-image: url("../images/icon-temp-active.svg");
+        }
+    }
+}

+ 12 - 0
packages/Longyi/Member/src/Resources/assets/images/icon-temp-active.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon-Catalog-Active</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Icon-Catalog-Active" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
+        <g transform="translate(9.000000, 7.000000)" stroke="#0041FF" stroke-width="2">
+            <rect id="Rectangle-2" x="0" y="0" width="30" height="34"></rect>
+        </g>
+    </g>
+</svg>

+ 12 - 0
packages/Longyi/Member/src/Resources/assets/images/icon-temp.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon-Catalog</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Icon-Catalog" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
+        <g transform="translate(9.000000, 7.000000)" stroke-width="2">
+            <rect id="Rectangle-2" stroke="#8E8E8E" x="0" y="0" width="30" height="34"></rect>
+        </g>
+    </g>
+</svg>

+ 4 - 0
packages/Longyi/Member/src/Resources/assets/js/app.js

@@ -0,0 +1,4 @@
+/**
+ * This will track all the images and fonts for publishing.
+ */
+import.meta.glob(["../images/**"]);

+ 9 - 0
packages/Longyi/Member/src/Resources/lang/en/app.php

@@ -0,0 +1,9 @@
+<?php
+
+return [
+    'member' => [
+        'discount' => 'Plus Fee',
+        'vip_active' => 'VIP 会员权益生效中',
+        'vip_discount' => 'VIP amount',
+    ],
+];

+ 9 - 0
packages/Longyi/Member/src/Resources/lang/zh_CN/app.php

@@ -0,0 +1,9 @@
+<?php
+
+return [
+    'member' => [
+        'discount' => 'Plus Fee',
+        'vip_active' => 'VIP 会员权益生效中',
+        'vip_discount' => 'VIP amount',
+    ],
+];

+ 19 - 0
packages/Longyi/Member/src/Resources/views/admin/index.blade.php

@@ -0,0 +1,19 @@
+<x-admin::layouts>
+    <x-slot:title>
+        vip管理
+    </x-slot>
+
+    @if(session('success'))
+        <div class="mb-4 px-4 py-3 bg-green-500 text-white rounded-lg">
+            {{ session('success') }}
+        </div>
+    @endif
+
+    <div class="flex justify-between items-center">
+        <p class="text-xl text-gray-800 font-bold">
+            vip管理列表
+        </p>
+    </div>
+
+    <x-admin::datagrid src="{{ route('admin.member.index') }}"></x-admin::datagrid>
+</x-admin::layouts>

+ 1 - 0
packages/Longyi/Member/src/Resources/views/admin/layouts/style.blade.php

@@ -0,0 +1 @@
+<link rel="stylesheet" href="{{ asset('themes/default/assets/css/admin.css') }}">

+ 16 - 0
packages/Longyi/Member/src/Resources/views/components/member-discount-cartsummary.blade.php

@@ -0,0 +1,16 @@
+{!! view_render_event('bagisto.shop.checkout.cart.summary.member_discount.before') !!}
+
+<div
+    class="flex text-right justify-between mt-2"
+    v-if="cart.vip_discount_amount && parseFloat(cart.vip_discount_amount) > 0"
+>
+    <p class="text-base">
+        @lang('member::app.member.vip_discount')
+    </p>
+
+    <p class="text-base font-medium">
+        - @{{ cart.formatted_vip_discount_amount }}
+    </p>
+</div>
+
+{!! view_render_event('bagisto.shop.checkout.cart.summary.member_discount.after') !!}

+ 183 - 0
packages/Longyi/Member/src/Resources/views/components/vip-member-discount-cartsummary.blade.php

@@ -0,0 +1,183 @@
+{!! view_render_event('bagisto.shop.checkout.cart.summary.vip_member_discount.before') !!}
+
+<v-member-discount
+    :cart="cart"
+    @member-discount-toggled="getCart"
+>
+</v-member-discount>
+
+@pushOnce('scripts')
+    <script type="text/x-template" id="v-member-discount-template">
+        <!-- VIP 用户显示提示信息 -->
+        <div v-if="isVipUser" class="flex justify-between items-center mt-2 p-3 bg-green-50 border border-green-200 rounded-lg">
+            <div class="flex items-center gap-2">
+                <span class="text-green-600 text-xl">✓</span>
+                <div>
+                    <p class="text-base font-medium text-green-800">
+                        @lang('member::app.member.vip_active')
+                    </p>
+                    <p class="text-xs text-green-600">
+                        @{{ vipExpireMessage }}
+                    </p>
+                </div>
+            </div>
+        </div>
+
+        <!-- 非 VIP 或 VIP 已过期用户显示优惠开关 -->
+        <div v-else class="flex justify-between items-center mt-2">
+            <div class="flex items-center gap-2">
+                <p class="text-base">
+                    @lang('member::app.member.discount')
+                </p>
+
+                <!-- Toggle Switch -->
+                <div
+                    class="relative inline-flex items-center cursor-pointer"
+                    @click="toggleSwitch"
+                    :class="{ 'opacity-50 cursor-not-allowed': !canApplyDiscount }"
+                >
+                    <!-- Background -->
+                    <div
+                        class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out"
+                        :class="isMemberDiscountApplied ? 'bg-blue-600' : 'bg-gray-300'"
+                    ></div>
+
+                    <!-- Knob -->
+                    <div
+                        class="absolute left-0.5 top-0.5 w-5 h-5 bg-white rounded-full shadow-md transition-transform duration-200 ease-in-out border border-gray-300"
+                        :class="isMemberDiscountApplied ? 'transform translate-x-5' : ''"
+                    ></div>
+
+                    <!-- Hidden checkbox for form submission -->
+                    <input
+                        type="checkbox"
+                        class="sr-only"
+                        :checked="isMemberDiscountApplied"
+                        :disabled="!canApplyDiscount"
+                    >
+                </div>
+            </div>
+
+            <p class="text-base font-medium" v-if="isMemberDiscountApplied && cart.formatted_vip_plus_amount">
+                + @{{ cart.formatted_vip_plus_amount }}
+            </p>
+        </div>
+    </script>
+
+    <script type="module">
+        app.component('v-member-discount', {
+            template: '#v-member-discount-template',
+
+            props: ['cart'],
+
+            data() {
+                return {
+                    isMemberDiscountApplied: false,
+                };
+            },
+
+            computed: {
+                isVipUser() {
+                    const isVip = this.cart && this.cart.vip_status && this.cart.vip_status.is_vip;
+                    return isVip;
+                },
+
+                vipExpireMessage() {
+                    if (!this.cart || !this.cart.vip_status) {
+                        return '';
+                    }
+                    const days = this.cart.vip_status.days_remaining;
+                    if (days > 0) {
+                        return `VIP 有效期还剩 ${days} 天`;
+                    }
+                    return 'VIP 已激活';
+                },
+
+                canApplyDiscount() {
+                    return this.cart && parseFloat(this.cart.grand_total) > 19.9;
+                }
+            },
+
+            watch: {
+                cart: {
+                    handler(newCart) {
+                        if (newCart) {
+                            // 只同步状态,不自动应用
+                            this.isMemberDiscountApplied = !!(newCart.vip_plus_amount && parseFloat(newCart.vip_plus_amount) > 0);
+                        }
+                    },
+                    immediate: true,
+                    deep: true
+                }
+            },
+
+            methods: {
+                toggleSwitch() {
+                    if (this.isMemberDiscountApplied) {
+                        this.removeMemberDiscount();
+                    } else {
+                        this.applyMemberDiscount();
+                    }
+                },
+
+                applyMemberDiscount() {
+                    this.$axios.post("{{ route('shop.api.checkout.cart.member.apply') }}", {
+                        _token: "{{ csrf_token() }}"
+                    })
+                        .then((response) => {
+                            if (response.data.success) {
+                                this.isMemberDiscountApplied = true;
+                                this.$emitter.emit('add-flash', {
+                                    type: 'success',
+                                    message: response.data.message || '会员优惠已应用'
+                                });
+                                this.$emit('member-discount-toggled');
+                            } else {
+                                this.$emitter.emit('add-flash', {
+                                    type: 'error',
+                                    message: response.data.message || '应用会员优惠失败'
+                                });
+                            }
+                        })
+                        .catch((error) => {
+                            console.error('Apply member discount error:', error);
+                            this.$emitter.emit('add-flash', {
+                                type: 'error',
+                                message: error.response?.data?.message || '应用会员优惠失败'
+                            });
+                        });
+                },
+
+                removeMemberDiscount() {
+                    this.$axios.post("{{ route('shop.api.checkout.cart.member.remove') }}", {
+                        _token: "{{ csrf_token() }}"
+                    })
+                        .then((response) => {
+                            if (response.data.success) {
+                                this.isMemberDiscountApplied = false;
+                                this.$emitter.emit('add-flash', {
+                                    type: 'success',
+                                    message: response.data.message || '会员优惠已移除'
+                                });
+                                this.$emit('member-discount-toggled');
+                            } else {
+                                this.$emitter.emit('add-flash', {
+                                    type: 'error',
+                                    message: response.data.message || '移除会员优惠失败'
+                                });
+                            }
+                        })
+                        .catch((error) => {
+                            console.error('Remove member discount error:', error);
+                            this.$emitter.emit('add-flash', {
+                                type: 'error',
+                                message: error.response?.data?.message || '移除会员优惠失败'
+                            });
+                        });
+                }
+            }
+        });
+    </script>
+@endPushOnce
+
+{!! view_render_event('bagisto.shop.checkout.cart.summary.vip_member_discount.after') !!}

+ 22 - 0
packages/Longyi/Member/src/Resources/views/customers/account/orders/member-info.blade.php

@@ -0,0 +1,22 @@
+@if (isset($order) && $order->vip_plus_amount && $order->vip_plus_amount > 0)
+    <div class="flex w-full justify-between gap-x-5">
+        <p>
+            @lang('member::app.member.discount')
+        </p>
+
+        <p>
+            + {{ core()->formatPrice($order->vip_plus_amount, $order->order_currency_code) }}
+        </p>
+    </div>
+@endif
+@if (isset($order) && $order->vip_discount_amount && $order->vip_discount_amount > 0)
+    <div class="flex w-full justify-between gap-x-5">
+        <p>
+            @lang('member::app.member.vip_discount')
+        </p>
+
+        <p>
+            - {{ core()->formatPrice($order->vip_discount_amount, $order->order_currency_code) }}
+        </p>
+    </div>
+@endif

+ 29 - 0
packages/Longyi/Member/src/Resources/views/sales/orders/view.blade.php

@@ -0,0 +1,29 @@
+{{-- Member Discount (19.9) --}}
+@if (isset($order) && $order->vip_plus_amount && $order->vip_plus_amount > 0)
+    <div class="flex w-full justify-between gap-x-5">
+        <p>
+            @lang('member::app.member.discount')
+        </p>
+
+        <p>
+            + {{ core()->formatBasePrice($order->base_vip_plus_amount) }}
+        </p>
+    </div>
+@endif
+
+{!! view_render_event('bagisto.admin.sales.order.view.vip_plus.after') !!}
+
+{!! view_render_event('bagisto.admin.sales.order.view.vip_discount_amount.before') !!}
+{{-- VIP Discount (3%) --}}
+@if (isset($order) && $order->vip_discount_amount && $order->vip_discount_amount > 0)
+    <div class="flex w-full justify-between gap-x-5">
+        <p>
+            @lang('member::app.member.vip_discount')
+        </p>
+
+        <p>
+            - {{ core()->formatBasePrice($order->base_vip_discount_amount) }}
+        </p>
+    </div>
+@endif
+{!! view_render_event('bagisto.admin.sales.order.view.vip_discount_amount.after') !!}

+ 11 - 0
packages/Longyi/Member/src/Resources/views/shop/index.blade.php

@@ -0,0 +1,11 @@
+<x-shop::layouts>
+
+    <!-- Title of the page -->
+    <x-slot:title>
+        Package Member
+    </x-slot>
+
+    <div class="main">
+        Package Member
+    </div>
+</x-shop::layouts>

+ 10 - 0
packages/Longyi/Member/src/Routes/admin-routes.php

@@ -0,0 +1,10 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\Member\Http\Controllers\Admin\MemberController;
+
+Route::group(['middleware' => ['web', 'admin'], 'prefix' => 'admin/member'], function () {
+    Route::controller(MemberController::class)->group(function () {
+        Route::get('', 'index')->name('admin.member.index');
+    });
+});

+ 14 - 0
packages/Longyi/Member/src/Routes/shop-routes.php

@@ -0,0 +1,14 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\Member\Http\Controllers\Shop\MemberController;
+use Longyi\Member\Http\Controllers\Shop\MemberDiscountController;
+
+Route::group(['middleware' => ['web', 'theme', 'locale', 'currency'], 'prefix' => 'member'], function () {
+    Route::get('', [MemberController::class, 'index'])->name('shop.member.index');
+});
+
+Route::group(['middleware' => ['web', 'theme', 'locale', 'currency'], 'prefix' => 'api/checkout/cart/member'], function () {
+    Route::post('apply', [MemberDiscountController::class, 'apply'])->name('shop.api.checkout.cart.member.apply');
+    Route::post('remove', [MemberDiscountController::class, 'remove'])->name('shop.api.checkout.cart.member.remove');
+});

+ 4 - 0
packages/Longyi/Member/tailwind.config.js

@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+    content: ["./src/Resources/**/*.blade.php", "./src/Resources/**/*.js"],
+};

+ 46 - 0
packages/Longyi/Member/vite.config.js

@@ -0,0 +1,46 @@
+import { defineConfig, loadEnv } from "vite";
+import vue from "@vitejs/plugin-vue";
+import laravel from "laravel-vite-plugin";
+import path from "path";
+
+export default defineConfig(({ mode }) => {
+    const envDir = "../../../";
+
+    Object.assign(process.env, loadEnv(mode, envDir));
+
+    return {
+        build: {
+            emptyOutDir: true,
+        },
+
+        envDir,
+
+        server: {
+            host: process.env.VITE_HOST || "localhost",
+            port: process.env.VITE_PORT || 5173,
+        },
+
+        plugins: [
+            vue(),
+
+            laravel({
+                hotFile: "../../../public/member-default-vite.hot",
+                publicDirectory: "../../../public",
+                buildDirectory: "themes/member/default/build",
+                input: [
+                    "src/Resources/assets/css/app.css",
+                    "src/Resources/assets/js/app.js",
+                ],
+                refresh: true,
+            }),
+        ],
+
+        experimental: {
+            renderBuiltUrl(filename, { hostId, hostType, type }) {
+                if (hostType === "css") {
+                    return path.basename(filename);
+                }
+            },
+        },
+    };
+});

+ 12 - 0
packages/Longyi/Pay/Afterpay/src/Config/payment-methods.php

@@ -0,0 +1,12 @@
+<?php
+
+return [
+    'afterpay'  => [
+        'code'        => 'afterpay',
+        'title'       => 'Afterpay',
+        'description' => 'Afterpay',
+        'class'       => 'Longyi\Pay\Afterpay\Payment\Afterpay',
+        'active'      => true,
+        'sort'        => 1,
+    ],
+];

+ 82 - 0
packages/Longyi/Pay/Afterpay/src/Config/system.php

@@ -0,0 +1,82 @@
+<?php
+
+return [
+    [
+        'key'    => 'sales.payment_methods.afterpay',
+        'name'   => 'Afterpay',
+        'info'   => 'Information about Afterpay',
+        'sort'   => 5,
+        'fields' => [
+            [
+                'name'          => 'title',
+                'title'         => 'Title',
+                'type'          => 'text',
+                'validation'    => 'required',
+                'channel_based' => false,
+                'locale_based'  => true,
+            ], [
+                'name'          => 'description',
+                'title'         => 'Description',
+                'type'          => 'textarea',
+                'channel_based' => false,
+                'locale_based'  => true,
+            ], [
+                'name' => 'mode',
+                'title' => 'Mode',
+                'type' => 'select',
+                'options' => [
+                    [
+                        'title' => 'live',
+                        'value' => 'live',
+                    ], [
+                        'title' => 'test',
+                        'value' => 'test',
+                    ],
+                ],
+                'depend' => 'active:1',
+                'validation' => 'required_if:active,1',
+                'channel_based' => false,
+                'locale_based' => true,
+            ], [
+                'name' => 'client_id',
+                'title' => 'ClientId',
+                'type' => 'text',
+                'depend' => 'active:1',
+                'validation' => 'required_if:active,1',
+                'channel_based' => false,
+                'locale_based' => true,
+            ], [
+                'name' => 'secret_id',
+                'title' => 'Secret',
+                'type' => 'password',
+                'depend' => 'active:1',
+                'validation' => 'required_if:active,1',
+                'channel_based' => false,
+                'locale_based' => true,
+            ], [
+                'name' => 'webhook_id',
+                'title' => 'WebhookId',
+                'type' => 'text',
+                'depend' => 'active:1',
+                'validation' => 'required_if:active,1',
+                'channel_based' => false,
+                'locale_based' => true,
+            ], [
+                'name'          => 'sort',
+                'title'         => 'Sort Order',
+                'type'          => 'text',
+                'validation'    => 'numeric',
+                'channel_based' => true,
+                'locale_based'  => false,
+                'default'       => 5,
+            ], [
+                'name'          => 'active',
+                'title'         => 'Status',
+                'type'          => 'boolean',
+                'validation'    => 'required',
+                'channel_based' => false,
+                'locale_based'  => true,
+            ]
+        ]
+    ]
+];

+ 0 - 0
packages/Longyi/Pay/Afterpay/src/Http/Controllers/AfterpayController.php


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است