Просмотр исходного кода

Merge branch 'dev-rewardPoints' into dev

# Conflicts:
#	bootstrap/providers.php
#	composer.lock
bianjunhui 9 часов назад
Родитель
Сommit
dfc844111d
42 измененных файлов с 5372 добавлено и 177 удалено
  1. 0 2
      bootstrap/cache/.gitignore
  2. 193 0
      bootstrap/cache/packages.php
  3. 392 0
      bootstrap/cache/services.php
  4. 1 0
      bootstrap/providers.php
  5. 2 0
      composer.json
  6. 113 1
      composer.lock
  7. 633 0
      packages/Longyi/ImageUpload/FRONTEND_EXAMPLES.md
  8. 191 0
      packages/Longyi/ImageUpload/INSTALLATION.md
  9. 152 0
      packages/Longyi/ImageUpload/QUICKSTART.md
  10. 507 0
      packages/Longyi/ImageUpload/README.md
  11. 31 0
      packages/Longyi/ImageUpload/composer.json
  12. 65 0
      packages/Longyi/ImageUpload/src/Config/image_upload.php
  13. 166 0
      packages/Longyi/ImageUpload/src/Http/Controllers/Admin/ImageUploadController.php
  14. 169 0
      packages/Longyi/ImageUpload/src/Http/Controllers/Shop/ImageUploadController.php
  15. 62 0
      packages/Longyi/ImageUpload/src/Providers/ImageUploadServiceProvider.php
  16. 25 0
      packages/Longyi/ImageUpload/src/Resources/lang/en/app.php
  17. 25 0
      packages/Longyi/ImageUpload/src/Resources/lang/zh_CN/app.php
  18. 291 0
      packages/Longyi/ImageUpload/src/Resources/views/admin/image-upload/index.blade.php
  19. 520 0
      packages/Longyi/ImageUpload/src/Resources/views/components/image-upload.blade.php
  20. 13 0
      packages/Longyi/ImageUpload/src/Routes/admin-routes.php
  21. 20 0
      packages/Longyi/ImageUpload/src/Routes/shop-routes.php
  22. 200 0
      packages/Longyi/ImageUpload/src/Services/ImageUploadService.php
  23. 188 0
      packages/Longyi/RewardPoints/EXTENSION_GUIDE.md
  24. 290 0
      packages/Longyi/RewardPoints/src/Config/TransactionType.php
  25. 5 3
      packages/Longyi/RewardPoints/src/Http/Controllers/Admin/CustomerController.php
  26. 6 24
      packages/Longyi/RewardPoints/src/Http/Controllers/Admin/RuleController.php
  27. 3 15
      packages/Longyi/RewardPoints/src/Http/Controllers/Admin/TransactionController.php
  28. 14 8
      packages/Longyi/RewardPoints/src/Listeners/CustomerEvents.php
  29. 3 2
      packages/Longyi/RewardPoints/src/Listeners/OrderEvents.php
  30. 44 20
      packages/Longyi/RewardPoints/src/Listeners/ReviewEvents.php
  31. 24 23
      packages/Longyi/RewardPoints/src/Models/RewardActiveRule.php
  32. 25 5
      packages/Longyi/RewardPoints/src/Repositories/RewardPointRepository.php
  33. 4 14
      packages/Longyi/RewardPoints/src/Resources/views/admin/customers/show.blade.php
  34. 15 31
      packages/Longyi/RewardPoints/src/Resources/views/admin/transactions/index.blade.php
  35. 3 2
      packages/Longyi/RewardPoints/src/Services/CartRewardPoints.php
  36. 69 6
      packages/Webkul/Admin/src/Http/Controllers/TinyMCEController.php
  37. 2 2
      packages/Webkul/Admin/src/Http/Requests/ProductForm.php
  38. 653 0
      packages/Webkul/BagistoApi/docs/GRAPHQL_REVIEW_S3_UPLOAD.md
  39. 22 2
      packages/Webkul/BagistoApi/src/Dto/CreateProductReviewInput.php
  40. 5 3
      packages/Webkul/BagistoApi/src/Providers/BagistoApiServiceProvider.php
  41. 186 6
      packages/Webkul/BagistoApi/src/State/ProductReviewProcessor.php
  42. 40 8
      packages/Webkul/Product/src/Repositories/ProductMediaRepository.php

+ 0 - 2
bootstrap/cache/.gitignore

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

+ 193 - 0
bootstrap/cache/packages.php

@@ -0,0 +1,193 @@
+<?php return array (
+  'astrotomic/laravel-translatable' => 
+  array (
+    'providers' => 
+    array (
+      0 => 'Astrotomic\\Translatable\\TranslatableServiceProvider',
+    ),
+  ),
+  '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-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',
+    ),
+  ),
+);

+ 392 - 0
bootstrap/cache/services.php

@@ -0,0 +1,392 @@
+<?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\\Faker\\Providers\\FakerServiceProvider',
+    25 => 'Barryvdh\\Debugbar\\ServiceProvider',
+    26 => 'Barryvdh\\DomPDF\\ServiceProvider',
+    27 => 'Diglactic\\Breadcrumbs\\ServiceProvider',
+    28 => 'Kalnoy\\Nestedset\\NestedSetServiceProvider',
+    29 => 'Konekt\\Concord\\ConcordServiceProvider',
+    30 => 'Konekt\\Enum\\Eloquent\\EnumServiceProvider',
+    31 => 'Laravel\\Octane\\OctaneServiceProvider',
+    32 => 'Laravel\\Sanctum\\SanctumServiceProvider',
+    33 => 'Laravel\\Tinker\\TinkerServiceProvider',
+    34 => 'Laravel\\Ui\\UiServiceProvider',
+    35 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
+    36 => 'Carbon\\Laravel\\ServiceProvider',
+    37 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
+    38 => 'Termwind\\Laravel\\TermwindServiceProvider',
+    39 => 'OpenAI\\Laravel\\ServiceProvider',
+    40 => 'Pest\\Laravel\\PestServiceProvider',
+    41 => 'Prettus\\Repository\\Providers\\RepositoryServiceProvider',
+    42 => 'Spatie\\ResponseCache\\ResponseCacheServiceProvider',
+    43 => 'Spatie\\Sitemap\\SitemapServiceProvider',
+    44 => 'Stevebauman\\Purify\\PurifyServiceProvider',
+    45 => 'ApiPlatform\\Laravel\\ApiPlatformProvider',
+    46 => 'ApiPlatform\\Laravel\\ApiPlatformDeferredProvider',
+    47 => 'ApiPlatform\\Laravel\\Eloquent\\ApiPlatformEventProvider',
+    48 => 'App\\Providers\\AppServiceProvider',
+    49 => 'Webkul\\Admin\\Providers\\AdminServiceProvider',
+    50 => 'Longyi\\Core\\Providers\\LongyiCoreServiceProvider',
+    51 => 'Longyi\\DynamicMenu\\Providers\\DynamicMenuServiceProvider',
+    52 => 'Longyi\\Email\\Providers\\EmailServiceProvider',
+    53 => 'Longyi\\RewardPoints\\Providers\\RewardPointsServiceProvider',
+    54 => 'Longyi\\ImageUpload\\Providers\\ImageUploadServiceProvider',
+    55 => 'Webkul\\Attribute\\Providers\\AttributeServiceProvider',
+    56 => 'Webkul\\BookingProduct\\Providers\\BookingProductServiceProvider',
+    57 => 'Webkul\\CMS\\Providers\\CMSServiceProvider',
+    58 => 'Webkul\\CartRule\\Providers\\CartRuleServiceProvider',
+    59 => 'Webkul\\CatalogRule\\Providers\\CatalogRuleServiceProvider',
+    60 => 'Webkul\\Category\\Providers\\CategoryServiceProvider',
+    61 => 'Webkul\\Checkout\\Providers\\CheckoutServiceProvider',
+    62 => 'Webkul\\Core\\Providers\\CoreServiceProvider',
+    63 => 'Webkul\\Core\\Providers\\EnvValidatorServiceProvider',
+    64 => 'Webkul\\Customer\\Providers\\CustomerServiceProvider',
+    65 => 'Webkul\\DataGrid\\Providers\\DataGridServiceProvider',
+    66 => 'Webkul\\DataTransfer\\Providers\\DataTransferServiceProvider',
+    67 => 'Webkul\\DebugBar\\Providers\\DebugBarServiceProvider',
+    68 => 'Webkul\\FPC\\Providers\\FPCServiceProvider',
+    69 => 'Webkul\\GDPR\\Providers\\GDPRServiceProvider',
+    70 => 'Webkul\\Installer\\Providers\\InstallerServiceProvider',
+    71 => 'Webkul\\Inventory\\Providers\\InventoryServiceProvider',
+    72 => 'Webkul\\MagicAI\\Providers\\MagicAIServiceProvider',
+    73 => 'Webkul\\Marketing\\Providers\\MarketingServiceProvider',
+    74 => 'Webkul\\Notification\\Providers\\NotificationServiceProvider',
+    75 => 'Webkul\\Payment\\Providers\\PaymentServiceProvider',
+    76 => 'Webkul\\Paypal\\Providers\\PaypalServiceProvider',
+    77 => 'Webkul\\Product\\Providers\\ProductServiceProvider',
+    78 => 'Webkul\\Rule\\Providers\\RuleServiceProvider',
+    79 => 'Webkul\\Sales\\Providers\\SalesServiceProvider',
+    80 => 'Webkul\\Shipping\\Providers\\ShippingServiceProvider',
+    81 => 'Webkul\\Shop\\Providers\\ShopServiceProvider',
+    82 => 'Webkul\\Sitemap\\Providers\\SitemapServiceProvider',
+    83 => 'Webkul\\SocialLogin\\Providers\\SocialLoginServiceProvider',
+    84 => 'Webkul\\SocialShare\\Providers\\SocialShareServiceProvider',
+    85 => 'Webkul\\Tax\\Providers\\TaxServiceProvider',
+    86 => 'Webkul\\Theme\\Providers\\ThemeServiceProvider',
+    87 => 'Webkul\\User\\Providers\\UserServiceProvider',
+    88 => '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\\Faker\\Providers\\FakerServiceProvider',
+    12 => 'Barryvdh\\Debugbar\\ServiceProvider',
+    13 => 'Barryvdh\\DomPDF\\ServiceProvider',
+    14 => 'Kalnoy\\Nestedset\\NestedSetServiceProvider',
+    15 => 'Konekt\\Concord\\ConcordServiceProvider',
+    16 => 'Konekt\\Enum\\Eloquent\\EnumServiceProvider',
+    17 => 'Laravel\\Octane\\OctaneServiceProvider',
+    18 => 'Laravel\\Sanctum\\SanctumServiceProvider',
+    19 => 'Laravel\\Ui\\UiServiceProvider',
+    20 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
+    21 => 'Carbon\\Laravel\\ServiceProvider',
+    22 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
+    23 => 'Termwind\\Laravel\\TermwindServiceProvider',
+    24 => 'Pest\\Laravel\\PestServiceProvider',
+    25 => 'Prettus\\Repository\\Providers\\RepositoryServiceProvider',
+    26 => 'Spatie\\ResponseCache\\ResponseCacheServiceProvider',
+    27 => 'Spatie\\Sitemap\\SitemapServiceProvider',
+    28 => 'Stevebauman\\Purify\\PurifyServiceProvider',
+    29 => 'ApiPlatform\\Laravel\\ApiPlatformProvider',
+    30 => 'ApiPlatform\\Laravel\\Eloquent\\ApiPlatformEventProvider',
+    31 => 'App\\Providers\\AppServiceProvider',
+    32 => 'Webkul\\Admin\\Providers\\AdminServiceProvider',
+    33 => 'Longyi\\Core\\Providers\\LongyiCoreServiceProvider',
+    34 => 'Longyi\\DynamicMenu\\Providers\\DynamicMenuServiceProvider',
+    35 => 'Longyi\\Email\\Providers\\EmailServiceProvider',
+    36 => 'Longyi\\RewardPoints\\Providers\\RewardPointsServiceProvider',
+    37 => 'Longyi\\ImageUpload\\Providers\\ImageUploadServiceProvider',
+    38 => 'Webkul\\Attribute\\Providers\\AttributeServiceProvider',
+    39 => 'Webkul\\BookingProduct\\Providers\\BookingProductServiceProvider',
+    40 => 'Webkul\\CMS\\Providers\\CMSServiceProvider',
+    41 => 'Webkul\\CartRule\\Providers\\CartRuleServiceProvider',
+    42 => 'Webkul\\CatalogRule\\Providers\\CatalogRuleServiceProvider',
+    43 => 'Webkul\\Category\\Providers\\CategoryServiceProvider',
+    44 => 'Webkul\\Checkout\\Providers\\CheckoutServiceProvider',
+    45 => 'Webkul\\Core\\Providers\\CoreServiceProvider',
+    46 => 'Webkul\\Core\\Providers\\EnvValidatorServiceProvider',
+    47 => 'Webkul\\Customer\\Providers\\CustomerServiceProvider',
+    48 => 'Webkul\\DataGrid\\Providers\\DataGridServiceProvider',
+    49 => 'Webkul\\DataTransfer\\Providers\\DataTransferServiceProvider',
+    50 => 'Webkul\\DebugBar\\Providers\\DebugBarServiceProvider',
+    51 => 'Webkul\\FPC\\Providers\\FPCServiceProvider',
+    52 => 'Webkul\\GDPR\\Providers\\GDPRServiceProvider',
+    53 => 'Webkul\\Installer\\Providers\\InstallerServiceProvider',
+    54 => 'Webkul\\Inventory\\Providers\\InventoryServiceProvider',
+    55 => 'Webkul\\MagicAI\\Providers\\MagicAIServiceProvider',
+    56 => 'Webkul\\Marketing\\Providers\\MarketingServiceProvider',
+    57 => 'Webkul\\Notification\\Providers\\NotificationServiceProvider',
+    58 => 'Webkul\\Payment\\Providers\\PaymentServiceProvider',
+    59 => 'Webkul\\Paypal\\Providers\\PaypalServiceProvider',
+    60 => 'Webkul\\Product\\Providers\\ProductServiceProvider',
+    61 => 'Webkul\\Rule\\Providers\\RuleServiceProvider',
+    62 => 'Webkul\\Sales\\Providers\\SalesServiceProvider',
+    63 => 'Webkul\\Shipping\\Providers\\ShippingServiceProvider',
+    64 => 'Webkul\\Shop\\Providers\\ShopServiceProvider',
+    65 => 'Webkul\\Sitemap\\Providers\\SitemapServiceProvider',
+    66 => 'Webkul\\SocialLogin\\Providers\\SocialLoginServiceProvider',
+    67 => 'Webkul\\SocialShare\\Providers\\SocialShareServiceProvider',
+    68 => 'Webkul\\Tax\\Providers\\TaxServiceProvider',
+    69 => 'Webkul\\Theme\\Providers\\ThemeServiceProvider',
+    70 => 'Webkul\\User\\Providers\\UserServiceProvider',
+    71 => '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 (
+    ),
+  ),
+);

+ 1 - 0
bootstrap/providers.php

@@ -13,6 +13,7 @@ return [
     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,

+ 2 - 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.25",
         "maatwebsite/excel": "^3.1.46",
         "mpdf/mpdf": "^8.2",
         "nesbot/carbon": "^2.72.2",
@@ -70,6 +71,7 @@
             "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/",

+ 113 - 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": "d8c84f1ab224d9264bc558050dc253b8",
     "packages": [
         {
             "name": "api-platform/documentation",
@@ -4591,6 +4591,57 @@
             },
             "time": "2024-10-08T08:58:34+00:00"
         },
+        {
+            "name": "league/flysystem-aws-s3-v3",
+            "version": "3.29.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
+                "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://repo.huaweicloud.com/repository/php/league/flysystem-aws-s3-v3/3.29.0/league-flysystem-aws-s3-v3-3.29.0.zip",
+                "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9",
+                "shasum": ""
+            },
+            "require": {
+                "aws/aws-sdk-php": "^3.295.10",
+                "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\\": ""
+                }
+            },
+            "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"
+            ],
+            "time": "2024-08-17T13:10:48+00:00"
+        },
         {
             "name": "league/flysystem-local",
             "version": "3.29.0",
@@ -5620,6 +5671,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",

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

+ 188 - 0
packages/Longyi/RewardPoints/EXTENSION_GUIDE.md

@@ -0,0 +1,188 @@
+# 积分类型扩展指南
+
+## 概述
+
+积分系统已重构为配置化设计,新增积分类型只需修改一个文件,无需改动其他代码。
+
+## 核心文件
+
+- **配置文件**: `src/Config/TransactionType.php` - 统一管理所有积分类型
+- **模型类**: `src/Models/RewardActiveRule.php` - 引用配置类的常量
+
+## 新增积分类型步骤
+
+### 1. 在 TransactionType.php 中添加新类型
+
+```php
+class TransactionType
+{
+    // ... 现有类型 ...
+    
+    /**
+     * 新类型:例如“完成任务”
+     */
+    const TASK_COMPLETE = 10;  // 使用下一个可用的ID
+    
+    public static function all(): array
+    {
+        return [
+            // ... 现有类型配置 ...
+            
+            self::TASK_COMPLETE => [
+                'code' => 'task_complete',      // 唯一代码标识
+                'name' => '完成任务',            // 显示名称
+                'description' => '完成指定任务获得积分',  // 描述
+                'icon' => 'icon-task',          // 图标类名
+                'color' => 'cyan',              // 颜色主题
+            ],
+        ];
+    }
+}
+```
+
+**注意**:
+- 类型 ID `10` 已预留给“积分过期”,用于系统自动处理过期积分
+- 类型 ID `11` 已预留给“兑换礼品卡”,用于积分兑换功能
+- 类型 ID `99` 已预留给“管理员操作”,不应在规则中使用
+- 这些特殊类型不会出现在规则配置的下拉选项中
+- 如果需要排除某些类型不出现在规则配置中,可以使用 `getRuleTypes()` 方法
+
+### 2. 在 RewardActiveRule.php 中添加常量引用(保持向后兼容)
+
+```php
+class RewardActiveRule extends Model
+{
+    // ... 现有常量 ...
+    const TYPE_TASK_COMPLETE = TransactionType::TASK_COMPLETE;
+}
+```
+
+### 3. 创建对应的事件监听器(如需要)
+
+```php
+// src/Listeners/TaskEvents.php
+namespace Longyi\RewardPoints\Listeners;
+
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use Longyi\RewardPoints\Models\RewardActiveRule;
+
+class TaskEvents
+{
+    protected $rewardPointRepository;
+
+    public function __construct(RewardPointRepository $rewardPointRepository)
+    {
+        $this->rewardPointRepository = $rewardPointRepository;
+    }
+
+    public function handleTaskComplete($event): void
+    {
+        // 获取任务完成规则
+        $rule = RewardActiveRule::active()
+            ->ofType(RewardActiveRule::TYPE_TASK_COMPLETE)
+            ->first();
+        
+        if (!$rule) {
+            return;
+        }
+        
+        // 检查规则是否适用
+        if (!$rule->isApplicableToCustomer($event->customer)) {
+            return;
+        }
+        
+        // 获取积分值
+        $points = $rule->getPointsForCustomer($event->customer);
+        
+        if ($points > 0) {
+            $this->rewardPointRepository->addPoints(
+                $event->customer->id,
+                RewardActiveRule::TYPE_TASK_COMPLETE,
+                $points,
+                null,
+                "完成任务: {$event->task_name}"
+            );
+        }
+    }
+}
+```
+
+### 4. 注册事件监听器
+
+在 `src/Providers/EventServiceProvider.php` 中注册:
+
+```php
+protected $listen = [
+    // ... 其他事件 ...
+    'App\Events\TaskCompleted' => [
+        'Longyi\RewardPoints\Listeners\TaskEvents@handleTaskComplete',
+    ],
+];
+```
+
+### 5. 在后台创建规则
+
+访问后台管理页面,创建新的积分规则:
+- 交易类型选择:"完成任务"
+- 设置积分值、客户群组等参数
+
+## 优势
+
+✅ **单一配置源**: 只需修改 `TransactionType.php`  
+✅ **向后兼容**: 保留原有常量引用方式  
+✅ **易于维护**: 类型信息集中管理  
+✅ **类型安全**: 提供完整的类型检查和验证方法  
+
+## 常用工具方法
+
+```php
+use Longyi\RewardPoints\Config\TransactionType;
+
+// 获取所有类型
+$allTypes = TransactionType::all();
+
+// 获取类型名称
+$name = TransactionType::getName(TransactionType::ORDER);  // "订单"
+
+// 获取类型代码
+$code = TransactionType::getCode(TransactionType::ORDER);  // "order"
+
+// 根据代码获取ID
+$id = TransactionType::getIdByCode('order');  // 3
+
+// 检查类型是否存在
+$exists = TransactionType::exists(3);  // true
+
+// 获取下拉选项
+$options = TransactionType::options();  // [1 => '签到', 2 => '注册', ...]
+```
+
+## 注意事项
+
+1. **ID 唯一性**: 确保新类型的 ID 不与现有类型冲突
+2. **Code 唯一性**: `code` 字段应该是唯一的英文标识符
+3. **数据库迁移**: 如果需要在数据库中存储新类型,确保 `type_of_transaction` 字段能容纳新值
+4. **缓存清理**: 修改后清除应用缓存以确保生效
+
+## 扩展示例:添加"游戏成就"类型
+
+```php
+// 1. 在 TransactionType.php 中添加
+const GAME_ACHIEVEMENT = 10;
+
+// 在 all() 方法中添加配置
+self::GAME_ACHIEVEMENT => [
+    'code' => 'game_achievement',
+    'name' => '游戏成就',
+    'description' => '完成游戏成就获得积分',
+],
+
+// 2. 在 RewardActiveRule.php 中添加
+const TYPE_GAME_ACHIEVEMENT = TransactionType::GAME_ACHIEVEMENT;
+
+// 3. 创建监听器处理游戏成就事件
+// 4. 注册事件
+// 5. 在后台创建规则
+```
+
+完成!无需修改任何其他代码。

+ 290 - 0
packages/Longyi/RewardPoints/src/Config/TransactionType.php

@@ -0,0 +1,290 @@
+<?php
+
+namespace Longyi\RewardPoints\Config;
+
+/**
+ * 积分交易类型配置类
+ *
+ * 统一管理所有积分交易类型,避免硬编码
+ * 新增类型只需在此类中添加,无需修改其他代码
+ */
+class TransactionType
+{
+    /**
+     * 签到
+     */
+    const SIGN_IN = 1;
+
+    /**
+     * 注册
+     */
+    const REGISTRATION = 2;
+
+    /**
+     * 订单
+     */
+    const ORDER = 3;
+
+    /**
+     * 评价
+     */
+    const REVIEW = 4;
+
+    /**
+     * 推荐
+     */
+    const REFERRAL = 5;
+
+    /**
+     * 生日
+     */
+    const BIRTHDAY = 6;
+
+    /**
+     * 分享
+     */
+    const SHARE = 7;
+
+    /**
+     * 订阅
+     */
+    const SUBSCRIBE = 8;
+
+    /**
+     * 登录
+     */
+    const LOGIN = 9;
+
+    /**
+     * 过期积分
+     */
+    const EXPIRED = 10;
+
+    /**
+     * 兑换礼品卡
+     */
+    const GIFT_CARD_REDEEM = 11;
+
+    /**
+     * 后台管理员修改
+     */
+    const ADMIN_ACTION = 99;
+
+    /**
+     * 取消订单退回积分
+     */
+    const ORDER_CANCEL_REFUND = 12;
+
+    /**
+     * 获取所有类型配置
+     *
+     * @return array [type_id => ['code' => string, 'name' => string, 'description' => string]]
+     */
+    public static function all(): array
+    {
+        return [
+            self::SIGN_IN => [
+                'code' => 'sign_in',
+                'name' => '签到',
+                'description' => '每日签到获得积分',
+                'icon' => 'icon-calendar',
+                'color' => 'blue',
+            ],
+            self::REGISTRATION => [
+                'code' => 'registration',
+                'name' => '注册',
+                'description' => '新用户注册奖励',
+                'icon' => 'icon-user',
+                'color' => 'green',
+            ],
+            self::ORDER => [
+                'code' => 'order',
+                'name' => '订单',
+                'description' => '下单消费获得积分',
+                'icon' => 'icon-shopping-cart',
+                'color' => 'blue',
+            ],
+            self::REVIEW => [
+                'code' => 'review',
+                'name' => '评价',
+                'description' => '商品评价奖励',
+                'icon' => 'icon-star',
+                'color' => 'yellow',
+            ],
+            self::REFERRAL => [
+                'code' => 'referral',
+                'name' => '推荐',
+                'description' => '推荐新用户奖励',
+                'icon' => 'icon-share',
+                'color' => 'indigo',
+            ],
+            self::BIRTHDAY => [
+                'code' => 'birthday',
+                'name' => '生日',
+                'description' => '生日礼物积分',
+                'icon' => 'icon-gift',
+                'color' => 'pink',
+            ],
+            self::SHARE => [
+                'code' => 'share',
+                'name' => '分享',
+                'description' => '分享商品或活动',
+                'icon' => 'icon-share-alt',
+                'color' => 'orange',
+            ],
+            self::SUBSCRIBE => [
+                'code' => 'subscribe',
+                'name' => '订阅',
+                'description' => '订阅邮件通讯',
+                'icon' => 'icon-envelope',
+                'color' => 'orange',
+            ],
+            self::LOGIN => [
+                'code' => 'login',
+                'name' => '登录',
+                'description' => '每日登录奖励',
+                'icon' => 'icon-login',
+                'color' => 'purple',
+            ],
+            self::EXPIRED => [
+                'code' => 'expired',
+                'name' => '积分过期',
+                'description' => '积分到期自动扣除',
+                'icon' => 'icon-clock',
+                'color' => 'red',
+            ],
+            self::GIFT_CARD_REDEEM => [
+                'code' => 'gift_card_redeem',
+                'name' => '兑换礼品卡',
+                'description' => '使用积分兑换礼品卡',
+                'icon' => 'icon-gift-card',
+                'color' => 'teal',
+            ],
+            self::ADMIN_ACTION => [
+                'code' => 'admin_action',
+                'name' => '管理员操作',
+                'description' => '后台管理员手动调整积分',
+                'icon' => 'icon-setting',
+                'color' => 'gray',
+            ],
+            self::ORDER_CANCEL_REFUND => [
+                'code' => 'order_cancel_refund',
+                'name' => '取消订单退回',
+                'description' => '订单取消后退回积分',
+                'icon' => 'icon-undo',
+                'color' => 'cyan',
+            ],
+        ];
+    }
+
+    /**
+     * 获取可用于规则配置的类型(排除管理员操作、过期、兑换等特殊类型)
+     *
+     * @return array
+     */
+    public static function getRuleTypes(): array
+    {
+        $all = self::all();
+
+        // 排除特殊类型:管理员操作(99)、过期(10)、兑换礼品卡(11)、取消订单退回(12)
+        $excludedTypes = [
+            self::ADMIN_ACTION,
+            self::EXPIRED,
+            self::GIFT_CARD_REDEEM,
+            self::ORDER_CANCEL_REFUND,
+        ];
+
+        foreach ($excludedTypes as $typeId) {
+            unset($all[$typeId]);
+        }
+
+        return $all;
+    }
+
+    /**
+     * 获取单个类型信息
+     *
+     * @param int $typeId
+     * @return array|null
+     */
+    public static function get(int $typeId): ?array
+    {
+        $all = self::all();
+        return $all[$typeId] ?? null;
+    }
+
+    /**
+     * 根据 code 获取类型 ID
+     *
+     * @param string $code
+     * @return int|null
+     */
+    public static function getIdByCode(string $code): ?int
+    {
+        foreach (self::all() as $id => $config) {
+            if ($config['code'] === $code) {
+                return $id;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 获取类型名称
+     *
+     * @param int $typeId
+     * @return string
+     */
+    public static function getName(int $typeId): string
+    {
+        $info = self::get($typeId);
+        return $info['name'] ?? '未知类型';
+    }
+
+    /**
+     * 获取类型代码
+     *
+     * @param int $typeId
+     * @return string
+     */
+    public static function getCode(int $typeId): string
+    {
+        $info = self::get($typeId);
+        return $info['code'] ?? 'unknown';
+    }
+
+    /**
+     * 检查类型是否存在
+     *
+     * @param int $typeId
+     * @return bool
+     */
+    public static function exists(int $typeId): bool
+    {
+        return isset(self::all()[$typeId]);
+    }
+
+    /**
+     * 获取所有类型选项(用于下拉选择)
+     *
+     * @return array [type_id => name]
+     */
+    public static function options(): array
+    {
+        $options = [];
+        foreach (self::all() as $id => $config) {
+            $options[$id] = $config['name'];
+        }
+        return $options;
+    }
+
+    /**
+     * 获取所有类型代码列表
+     *
+     * @return array
+     */
+    public static function codes(): array
+    {
+        return array_column(self::all(), 'code');
+    }
+}

+ 5 - 3
packages/Longyi/RewardPoints/src/Http/Controllers/Admin/CustomerController.php

@@ -7,6 +7,7 @@ use Webkul\Admin\Http\Controllers\Controller;
 use Longyi\RewardPoints\Repositories\RewardPointRepository;
 use Longyi\RewardPoints\Models\RewardPointCustomer;
 use Longyi\RewardPoints\Models\RewardActiveRule;
+use Longyi\RewardPoints\Config\TransactionType;
 use Webkul\Customer\Models\Customer;
 use Carbon\Carbon;
 
@@ -125,10 +126,10 @@ class CustomerController extends Controller
             if ($action === 'add') {
                 $result = $this->rewardPointRepository->addPoints(
                     $customer->id,
-                    99, // 99 for admin action
+                    TransactionType::ADMIN_ACTION,
                     $points,
-                    null,
-                    "Admin: " . $reason
+                    "Admin: " . $reason,
+                    null
                 );
 
                 if ($result) {
@@ -140,6 +141,7 @@ class CustomerController extends Controller
                 $result = $this->rewardPointRepository->deductPoints(
                     $customer->id,
                     $points,
+                    TransactionType::ADMIN_ACTION,
                     null,
                     "Admin: " . $reason
                 );

+ 6 - 24
packages/Longyi/RewardPoints/src/Http/Controllers/Admin/RuleController.php

@@ -10,31 +10,13 @@ use Illuminate\Validation\Rule;
 use Illuminate\View\View;
 use Longyi\RewardPoints\Models\RewardActiveRule;
 use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use Longyi\RewardPoints\Config\TransactionType;
 use Webkul\Admin\Http\Controllers\Controller;
 use Webkul\Core\Models\Channel;
 use Webkul\Customer\Models\CustomerGroup;
 
 class RuleController extends Controller
 {
-    /**
-     * Transaction type mapping with complete configuration
-     */
-    private const TRANSACTION_TYPES = [
-        3 => ['name' => 'Order', 'icon' => 'icon-shopping-cart', 'color' => 'blue'],
-        2 => ['name' => 'Registration', 'icon' => 'icon-user', 'color' => 'green'],
-        4 => ['name' => 'Product Review', 'icon' => 'icon-star', 'color' => 'yellow'],
-        9 => ['name' => 'Login', 'icon' => 'icon-calendar', 'color' => 'purple'],
-        5 => ['name' => 'Referral', 'icon' => 'icon-share', 'color' => 'indigo'],
-        6 => ['name' => 'Birthday', 'icon' => 'icon-gift', 'color' => 'pink'],
-        7 => ['name' => 'Share', 'icon' => 'icon-share-alt', 'color' => 'orange'],
-        8 => ['name' => 'Subscribe', 'icon' => 'icon-envelope', 'color' => 'orange'],
-    ];
-
-    /**
-     * Valid transaction type IDs
-     */
-    private const VALID_TRANSACTION_TYPES = [1, 2, 3, 4, 5, 6, 7, 8];
-
     /**
      * Color classes mapping for transaction types
      */
@@ -69,7 +51,7 @@ class RuleController extends Controller
             ->paginate(15);
 
         $customerGroupsList = $this->getCustomerGroups();
-        $transactionTypes = self::TRANSACTION_TYPES;
+        $transactionTypes = TransactionType::getRuleTypes();
         $colorClasses = self::COLOR_CLASSES;
 
         $view = $this->_config['view'] ?? 'rewardpoints::admin.rules.index';
@@ -85,7 +67,7 @@ class RuleController extends Controller
         $view = $this->_config['view'] ?? 'rewardpoints::admin.rules.create';
 
         return view($view, [
-            'transactionTypes' => self::TRANSACTION_TYPES,
+            'transactionTypes' => TransactionType::getRuleTypes(),
             'storeViews' => $this->getStoreViews(),
             'customerGroups' => $this->getCustomerGroups(),
             'colorClasses' => self::COLOR_CLASSES,
@@ -121,7 +103,7 @@ class RuleController extends Controller
 
         return view($view, [
             'rule' => $rule,
-            'transactionTypes' => self::TRANSACTION_TYPES,
+            'transactionTypes' => TransactionType::getRuleTypes(),
             'storeViews' => $this->getStoreViews(),
             'customerGroups' => $this->getCustomerGroups(),
             'selectedStoreViewsForSelect' => $selectedStoreViewsForSelect,
@@ -235,7 +217,7 @@ class RuleController extends Controller
     {
         $rules = [
             'rule_name' => 'required|string|max:255',
-            'type_of_transaction' => ['required', 'integer', Rule::in(self::VALID_TRANSACTION_TYPES)],
+            'type_of_transaction' => ['required', 'integer', Rule::in(array_keys(TransactionType::getRuleTypes()))],
             'status' => 'required|boolean',
             'enable_different_points_by_group' => 'nullable|boolean',
             'expired_day' => 'nullable|integer|min:0',
@@ -254,7 +236,7 @@ class RuleController extends Controller
     {
         $rules = [
             'rule_name' => 'required|string|max:255',
-            'type_of_transaction' => ['required', 'integer', Rule::in(self::VALID_TRANSACTION_TYPES)],
+            'type_of_transaction' => ['required', 'integer', Rule::in(array_keys(TransactionType::getRuleTypes()))],
             'status' => 'required|boolean',
             'enable_different_points_by_group' => 'nullable|boolean',
             'default_expired' => 'nullable|boolean',

+ 3 - 15
packages/Longyi/RewardPoints/src/Http/Controllers/Admin/TransactionController.php

@@ -6,6 +6,7 @@ use Illuminate\Http\Request;
 use Webkul\Admin\Http\Controllers\Controller;
 use Longyi\RewardPoints\Models\RewardPointHistory;
 use Longyi\RewardPoints\Models\RewardPointCustomer;
+use Longyi\RewardPoints\Config\TransactionType;
 use Carbon\Carbon;
 use Maatwebsite\Excel\Facades\Excel;
 use Longyi\RewardPoints\Exports\TransactionsExport;
@@ -93,7 +94,7 @@ class TransactionController extends Controller
             'totalEarned',
             'totalRedeemed',
             'totalTransactions'
-        ));
+        ))->with('transactionTypes', TransactionType::all());
     }
 
     /**
@@ -158,19 +159,6 @@ class TransactionController extends Controller
                 'Status'
             ]);
 
-            $typeNames = [
-                3 => 'Order',
-                2 => 'Registration',
-                4 => 'Product Review',
-                1 => 'Daily Sign In',
-                5 => 'Referral',
-                6 => 'Birthday',
-                7 => 'Share',
-                8 => 'Subscribe',
-                9 => 'login',
-                99 => 'Admin Action'
-            ];
-
             $statusNames = [
                 0 => 'Pending',
                 1 => 'Completed',
@@ -184,7 +172,7 @@ class TransactionController extends Controller
                     $transaction->customer_id,
                     $transaction->customer ? $transaction->customer->first_name . ' ' . $transaction->customer->last_name : 'N/A',
                     $transaction->customer ? $transaction->customer->email : 'N/A',
-                    $typeNames[$transaction->type_of_transaction] ?? 'Unknown',
+                    TransactionType::getName($transaction->type_of_transaction),
                     $transaction->amount,
                     $transaction->balance,
                     $transaction->transaction_time,

+ 14 - 8
packages/Longyi/RewardPoints/src/Listeners/CustomerEvents.php

@@ -30,16 +30,22 @@ class CustomerEvents
         $pointsList = [];
 
         // 注册积分
-        $registrationPoints = $this->getRegistrationPoints($customer);
-        if ($registrationPoints > 0) {
-            $pointsList[] = $this->buildPointsItem($registrationPoints, RewardActiveRule::TYPE_REGISTRATION, 'Registration bonus');
+        $registrationRule = RewardActiveRule::active()->ofType(RewardActiveRule::TYPE_REGISTRATION)->first();
+        if ($registrationRule) {
+            $registrationPoints = $this->getPointsFromRuleOrConfig($registrationRule, 'rewardpoints.registration.points_per_registration', 100);
+            if ($registrationPoints > 0) {
+                $pointsList[] = $this->buildPointsItem($registrationPoints, RewardActiveRule::TYPE_REGISTRATION, 'Registration bonus', $registrationRule);
+            }
         }
 
         // 订阅积分(仅当用户订阅了新闻通讯)
         if ($this->isSubscribedToNewsletter($customer)) {
-            $subscribePoints = $this->getSubscribePoints($customer);
-            if ($subscribePoints > 0) {
-                $pointsList[] = $this->buildPointsItem($subscribePoints, RewardActiveRule::TYPE_SUBSCRIBE, 'Newsletter subscription bonus');
+            $subscribeRule = RewardActiveRule::active()->ofType(RewardActiveRule::TYPE_SUBSCRIBE)->first();
+            if ($subscribeRule) {
+                $subscribePoints = $this->getPointsFromRuleOrConfig($subscribeRule, 'rewardpoints.newsletter_subscribe.points_per_subscription', 200);
+                if ($subscribePoints > 0) {
+                    $pointsList[] = $this->buildPointsItem($subscribePoints, RewardActiveRule::TYPE_SUBSCRIBE, 'Newsletter subscription bonus', $subscribeRule);
+                }
             }
         }
 
@@ -192,13 +198,13 @@ class CustomerEvents
     /**
      * 构建积分项
      */
-    protected function buildPointsItem(int $amount, int $type, string $detail): array
+    protected function buildPointsItem(int $amount, int $type, string $detail, ?RewardActiveRule $rule = null): array
     {
         return [
             'amount' => $amount,
             'type' => $type,
             'detail' => $detail,
-            'rule' => null,
+            'rule' => $rule,
         ];
     }
 

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

@@ -136,8 +136,9 @@ class OrderEvents
             $this->rewardPointRepository->deductPoints(
                 $order->customer_id,
                 $history->point_remaining,
-                $order->id,
-                "Points deducted due to order cancellation #{$order->increment_id}"
+                RewardActiveRule::TYPE_ORDER_CANCEL_REFUND,
+                "Points refunded due to order cancellation #{$order->increment_id}",
+                $order->id
             );
         }
 

+ 44 - 20
packages/Longyi/RewardPoints/src/Listeners/ReviewEvents.php

@@ -6,6 +6,7 @@ use Longyi\RewardPoints\Repositories\RewardPointRepository;
 use Longyi\RewardPoints\Models\RewardActiveRule;
 use Webkul\Customer\Models\Customer;
 use Illuminate\Support\Facades\Log;
+use Carbon\Carbon;
 
 class ReviewEvents
 {
@@ -32,19 +33,19 @@ class ReviewEvents
         // 获取客户信息
         $customer = $this->getCustomer($review->customer_id);
         if (!$customer) {
-            Log::warning('Customer not found for review points', [
-                'review_id' => $review->id,
-                'customer_id' => $review->customer_id
-            ]);
+            // Log::warning('Customer not found for review points', [
+            //     'review_id' => $review->id,
+            //     'customer_id' => $review->customer_id
+            // ]);
             return;
         }
 
         // 【简化】只需要调用 isApplicableToCustomer,它会内部处理所有适用性检查
         if (!$rule->isApplicableToCustomer($customer)) {
-            Log::info('Review rule not applicable', [
-                'review_id' => $review->id,
-                'customer_id' => $customer->id,
-            ]);
+            // Log::info('Review rule not applicable', [
+            //     'review_id' => $review->id,
+            //     'customer_id' => $customer->id,
+            // ]);
             return;
         }
 
@@ -52,19 +53,28 @@ class ReviewEvents
         $points = $rule->getPointsForCustomer($customer);
 
         if ($points <= 0) {
-            Log::info('Review points is 0, skipping', [
-                'review_id' => $review->id,
-                'points' => $points
-            ]);
+            // Log::info('Review points is 0, skipping', [
+            //     'review_id' => $review->id,
+            //     'points' => $points
+            // ]);
             return;
         }
 
         // 检查是否已经为该评价发放过积分
         if ($this->hasReceivedReviewPoints($review->id, $customer->id)) {
-            Log::info('Review points already granted', [
+            /*Log::info('Review points already granted', [
                 'review_id' => $review->id,
                 'customer_id' => $customer->id
-            ]);
+            ]);*/
+            return;
+        }
+
+        // 检查今日是否已获得评价积分(一个用户一天只发放一次)
+        if ($this->hasReceivedReviewPointsToday($customer->id)) {
+            /*Log::info('Customer already received review points today', [
+                'review_id' => $review->id,
+                'customer_id' => $customer->id
+            ]);*/
             return;
         }
 
@@ -78,11 +88,11 @@ class ReviewEvents
             $rule
         );
 
-        Log::info('Review points added', [
+        /*Log::info('Review points added', [
             'review_id' => $review->id,
             'customer_id' => $customer->id,
             'points' => $points
-        ]);
+        ]);*/
     }
 
     /**
@@ -95,9 +105,9 @@ class ReviewEvents
         }
 
         if (empty($review->customer_id)) {
-            Log::info('Review has no customer_id', [
-                'review_id' => $review->id ?? null
-            ]);
+            // Log::info('Review has no customer_id', [
+            //     'review_id' => $review->id ?? null
+            // ]);
             return false;
         }
 
@@ -130,7 +140,21 @@ class ReviewEvents
         // 通过备注字段判断(需要存储评价ID)
         return \Longyi\RewardPoints\Models\RewardPointHistory::where('customer_id', $customerId)
             ->where('type_of_transaction', RewardActiveRule::TYPE_REVIEW)
-            ->where('description', 'LIKE', "%Review ID: {$reviewId}%")
+            ->where('transaction_detail', 'LIKE', "%Review ID: {$reviewId}%")
+            ->exists();
+    }
+
+    /**
+     * 检查今日是否已获得评价积分
+     */
+    protected function hasReceivedReviewPointsToday(int $customerId): bool
+    {
+        $today = Carbon::now()->format('Y-m-d');
+
+        return \Longyi\RewardPoints\Models\RewardPointHistory::where('customer_id', $customerId)
+            ->where('type_of_transaction', RewardActiveRule::TYPE_REVIEW)
+            ->whereDate('transaction_time', $today)
+            ->where('status', \Longyi\RewardPoints\Models\RewardPointHistory::STATUS_COMPLETED)
             ->exists();
     }
 

+ 24 - 23
packages/Longyi/RewardPoints/src/Models/RewardActiveRule.php

@@ -3,20 +3,25 @@
 namespace Longyi\RewardPoints\Models;
 
 use Illuminate\Database\Eloquent\Model;
+use Longyi\RewardPoints\Config\TransactionType;
 use Webkul\Customer\Models\Customer;
 
 class RewardActiveRule extends Model
 {
-    // 定义交易类型常量
-    const TYPE_SIGN_IN = 1;
-    const TYPE_REGISTRATION = 2;
-    const TYPE_ORDER = 3;
-    const TYPE_REVIEW = 4;
-    const TYPE_REFERRAL = 5;
-    const TYPE_BIRTHDAY = 6;
-    const TYPE_SHARE = 7;
-    const TYPE_SUBSCRIBE = 8;
-    const TYPE_LOGIN = 9;
+    // 使用配置类中的常量(保持向后兼容)
+    const TYPE_SIGN_IN = TransactionType::SIGN_IN;
+    const TYPE_REGISTRATION = TransactionType::REGISTRATION;
+    const TYPE_ORDER = TransactionType::ORDER;
+    const TYPE_REVIEW = TransactionType::REVIEW;
+    const TYPE_REFERRAL = TransactionType::REFERRAL;
+    const TYPE_BIRTHDAY = TransactionType::BIRTHDAY;
+    const TYPE_SHARE = TransactionType::SHARE;
+    const TYPE_SUBSCRIBE = TransactionType::SUBSCRIBE;
+    const TYPE_LOGIN = TransactionType::LOGIN;
+    const TYPE_EXPIRED = TransactionType::EXPIRED;
+    const TYPE_GIFT_CARD_REDEEM = TransactionType::GIFT_CARD_REDEEM;
+    const TYPE_ADMIN_ACTION = TransactionType::ADMIN_ACTION;
+    const TYPE_ORDER_CANCEL_REFUND = TransactionType::ORDER_CANCEL_REFUND;
 
     protected $table = 'mw_reward_active_rules';
     protected $primaryKey = 'rule_id';
@@ -184,19 +189,15 @@ class RewardActiveRule extends Model
      */
     public function getTransactionTypeTextAttribute(): string
     {
-        $types = [
-            self::TYPE_ORDER => 'Order',
-            self::TYPE_REGISTRATION => 'Registration',
-            self::TYPE_REVIEW => 'Product Review',
-            self::TYPE_LOGIN => 'Login',
-            self::TYPE_REFERRAL => 'Referral',
-            self::TYPE_BIRTHDAY => 'Birthday',
-            self::TYPE_SHARE => 'Share',
-            self::TYPE_SUBSCRIBE => 'Subscription',
-            self::TYPE_SIGN_IN => 'Sign In',
-        ];
-
-        return $types[$this->type_of_transaction] ?? 'Unknown';
+        return TransactionType::getName($this->type_of_transaction);
+    }
+
+    /**
+     * 获取交易类型代码
+     */
+    public function getTransactionTypeCodeAttribute(): string
+    {
+        return TransactionType::getCode($this->type_of_transaction);
     }
 
     /**

+ 25 - 5
packages/Longyi/RewardPoints/src/Repositories/RewardPointRepository.php

@@ -204,10 +204,10 @@ class RewardPointRepository extends Repository
         }
     }
 
-    public function deductPoints($customerId, $amount, $orderId = null, $detail = null)
+    public function deductPoints($customerId, $amount, $type = 0, $detail = null, $orderId = null)
     {
         try {
-            return DB::transaction(function () use ($customerId, $amount, $orderId, $detail) {
+            return DB::transaction(function () use ($customerId, $amount, $orderId, $detail,$type) {
                 $customerPoints = RewardPointCustomer::where('customer_id', $customerId)
                     ->lockForUpdate()
                     ->first();
@@ -237,7 +237,7 @@ class RewardPointRepository extends Repository
                 // 创建历史记录
                 $history = $this->create([
                     'customer_id' => $customerId,
-                    'type_of_transaction' => 0,
+                    'type_of_transaction' => $type,
                     'amount' => -$amount,
                     'balance' => $newBalance,
                     'transaction_detail' => $detail,
@@ -296,17 +296,37 @@ class RewardPointRepository extends Repository
                         ->first();
 
                     if ($customerPoints && $customerPoints->mw_reward_point >= $history->point_remaining) {
-                        $customerPoints->mw_reward_point -= $history->point_remaining;
+                        $expiredAmount = $history->point_remaining;
+
+                        // 扣除用户总积分
+                        $customerPoints->mw_reward_point -= $expiredAmount;
                         $customerPoints->save();
 
+                        // 更新原历史记录状态为过期
                         $history->status = RewardPointHistory::STATUS_EXPIRED;
                         $history->point_remaining = 0;
                         $history->save();
 
+                        // 创建一条新的过期扣减记录
+                        $this->create([
+                            'customer_id' => $history->customer_id,
+                            'type_of_transaction' => \Longyi\RewardPoints\Config\TransactionType::EXPIRED,
+                            'amount' => -$expiredAmount,
+                            'balance' => (int) $customerPoints->mw_reward_point,
+                            'transaction_detail' => '积分过期自动扣除',
+                            'transaction_time' => Carbon::now(),
+                            'history_order_id' => 0,
+                            'expired_day' => 0,
+                            'expired_time' => null,
+                            'point_remaining' => 0,
+                            'check_time' => 1,
+                            'status' => RewardPointHistory::STATUS_COMPLETED
+                        ]);
+
                         Log::info('Expired points processed', [
                             'history_id' => $history->history_id,
                             'customer_id' => $history->customer_id,
-                            'points_expired' => $history->getOriginal('point_remaining')
+                            'points_expired' => $expiredAmount
                         ]);
                     }
                 });

+ 4 - 14
packages/Longyi/RewardPoints/src/Resources/views/admin/customers/show.blade.php

@@ -86,22 +86,12 @@
                     <tr class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
                         <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
                             @php
-                                $typeNames = [
-                                    1 => 'Daily Sign In',
-                                    2 => 'Registration',
-                                    3 => 'Order',
-                                    4 => 'Product Review',
-                                    5 => 'Referral',
-                                    6 => 'Birthday',
-                                    7 => 'Share',
-                                    8 => 'Subscribe',
-                                    9 => 'Login',
-                                    99 => 'Admin Action'
-                                ];
+                                $typeInfo = \Longyi\RewardPoints\Config\TransactionType::get($record->type_of_transaction);
+                                $typeName = $typeInfo['name'] ?? 'Unknown';
                             @endphp
                             <span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
-                                    {{ $typeNames[$record->type_of_transaction] ?? 'Unknown' }}
-                                </span>
+                                {{ $typeName }}
+                            </span>
                         </td>
                         <td class="px-6 py-4 whitespace-nowrap text-sm">
                                 <span class="font-semibold {{ $record->amount > 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400' }}">

+ 15 - 31
packages/Longyi/RewardPoints/src/Resources/views/admin/transactions/index.blade.php

@@ -103,16 +103,11 @@
                     class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
                 >
                     <option value="">@lang('All Types')</option>
-                    <option value="3" {{ $transactionType == '3' ? 'selected' : '' }}>@lang('Order')</option>
-                    <option value="2" {{ $transactionType == '2' ? 'selected' : '' }}>@lang('Registration')</option>
-                    <option value="4" {{ $transactionType == '4' ? 'selected' : '' }}>@lang('Product Review')</option>
-                    <option value="1" {{ $transactionType == '1' ? 'selected' : '' }}>@lang('Daily Sign In')</option>
-                    <option value="5" {{ $transactionType == '5' ? 'selected' : '' }}>@lang('Referral')</option>
-                    <option value="6" {{ $transactionType == '6' ? 'selected' : '' }}>@lang('Birthday')</option>
-                    <option value="7" {{ $transactionType == '7' ? 'selected' : '' }}>@lang('Share')</option>
-                    <option value="8" {{ $transactionType == '8' ? 'selected' : '' }}>@lang('Subscribe')</option>
-                    <option value="9" {{ $transactionType == '9' ? 'selected' : '' }}>@lang('Login')</option>
-                    <option value="99" {{ $transactionType == '99' ? 'selected' : '' }}>@lang('Admin Action')</option>
+                    @foreach($transactionTypes as $typeId => $typeInfo)
+                        <option value="{{ $typeId }}" {{ $transactionType == $typeId ? 'selected' : '' }}>
+                            @lang($typeInfo['name'])
+                        </option>
+                    @endforeach
                 </select>
             </div>
 
@@ -259,27 +254,16 @@
                         </td>
                         <td class="px-6 py-4 whitespace-nowrap">
                             @php
-                                $typeNames = [
-                                        3 => 'Order',
-                                        2 => 'Registration',
-                                        4 => 'Product Review',
-                                        1 => 'Daily Sign In',
-                                        5 => 'Referral',
-                                        6 => 'Birthday',
-                                        7 => 'Share',
-                                        8 => 'Subscribe',
-                                        9 => 'login',
-                                        99 => 'Admin Action'
-                                    ];
+                                $typeInfo = \Longyi\RewardPoints\Config\TransactionType::get($transaction->type_of_transaction);
+                                $typeName = $typeInfo['name'] ?? 'Unknown';
+                                $typeColor = match ($transaction->amount > 0) {
+                                    true => 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
+                                    false => 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
+                                };
                             @endphp
-                            <span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full
-            @if($transaction->amount > 0)
-                bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200
-            @else
-                bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200
-            @endif">
-        @lang($typeNames[$transaction->type_of_transaction] ?? 'Unknown')
-    </span>
+                            <span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full {{ $typeColor }}">
+                                @lang($typeName)
+                            </span>
                         </td>
                         <td class="px-6 py-4 whitespace-nowrap text-sm">
                                 <span class="font-semibold
@@ -327,7 +311,7 @@
         {{-- 分页 --}}
         @if($transactions->hasPages())
             <div class="p-4 border-t border-gray-200 dark:border-gray-800">
-                {{ $transactions->links() }}
+                {{ $transactions->appends(request()->query())->links() }}
             </div>
         @endif
     </div>

+ 3 - 2
packages/Longyi/RewardPoints/src/Services/CartRewardPoints.php

@@ -203,8 +203,9 @@ class CartRewardPoints
         $result = $this->rewardPointRepository->deductPoints(
             $customerId,
             $pointsUsed,
-            $order->id,
-            "Points redeemed for order #{$order->increment_id} (Discount: " . core()->formatPrice($discountAmount) . ")"
+            0,
+            "Points redeemed for order #{$order->increment_id} (Discount: " . core()->formatPrice($discountAmount) . ")",
+            $order->id
         );
         
         // Clear session

+ 69 - 6
packages/Webkul/Admin/src/Http/Controllers/TinyMCEController.php

@@ -2,19 +2,27 @@
 
 namespace Webkul\Admin\Http\Controllers;
 
-use Illuminate\Support\Facades\Storage;
+use Aws\S3\S3Client;
+use Illuminate\Support\Str;
 use Webkul\Core\Traits\Sanitizer;
 
 class TinyMCEController extends Controller
 {
     use Sanitizer;
 
+    /**
+     * S3 client instance.
+     *
+     * @var S3Client|null
+     */
+    protected $s3Client = null;
+
     /**
      * Storage folder path.
      *
      * @var string
      */
-    private $storagePath = 'tinymce';
+    private $storagePath = 'uploads';
 
     /**
      * Allowed image MIME types.
@@ -30,6 +38,30 @@ class TinyMCEController extends Controller
         'image/webp',
     ];
 
+    /**
+     * Get S3 client instance (lazy-loaded).
+     *
+     * @return S3Client
+     */
+    protected function getS3Client()
+    {
+        if (is_null($this->s3Client)) {
+            $this->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,
+                ],
+            ]);
+        }
+
+        return $this->s3Client;
+    }
+
     /**
      * Upload file from tinymce.
      *
@@ -57,7 +89,7 @@ class TinyMCEController extends Controller
     }
 
     /**
-     * Store media.
+     * Store media to S3.
      *
      * @return array
      */
@@ -90,14 +122,45 @@ class TinyMCEController extends Controller
             return ['error' => trans('admin::app.components.tinymce.errors.file-extension-mismatch')];
         }
 
-        $path = $file->store($this->storagePath);
+        // 生成 S3 存储路径
+        $fileName = Str::random(40) . '.' . $extension;
+        $path = $this->storagePath . '/' . date('Ym/d') . '/' . $fileName;
+
+        $body = file_get_contents($file->getRealPath());
+
+        // SVG 需要先消毒再上传
+        if ($mimeType === 'image/svg+xml') {
+            $body = $this->sanitizeSvgContent($body);
+        }
+
+        // 上传到 S3
+        $this->getS3Client()->putObject([
+            'Bucket'      => env('AWS_BUCKET'),
+            'Key'         => $path,
+            'Body'        => $body,
+            'ContentType' => $mimeType,
+        ]);
 
-        $this->sanitizeSVG($path, $mimeType);
+        $fileUrl = rtrim(env('AWS_URL', ''), '/') . '/' . ltrim($path, '/');
 
         return [
             'file'      => $path,
             'file_name' => $file->getClientOriginalName(),
-            'file_url'  => Storage::url($path),
+            'file_url'  => $fileUrl,
         ];
     }
+
+    /**
+     * Sanitize SVG content (direct string version for S3 workflow).
+     *
+     * @param  string  $content
+     * @return string
+     */
+    protected function sanitizeSvgContent(string $content): string
+    {
+        $sanitizer = new \enshrined\svgSanitize\Sanitizer();
+        $sanitizer->removeRemoteReferences(true);
+
+        return $sanitizer->sanitize($content);
+    }
 }

+ 2 - 2
packages/Webkul/Admin/src/Http/Requests/ProductForm.php

@@ -75,7 +75,7 @@ class ProductForm extends FormRequest
         $this->rules = array_merge($this->product->getTypeInstance()->getTypeValidationRules(), [
             'sku'                  => ['required', 'unique:products,sku,'.$this->id, new Slug],
             'url_key'              => ['required', new ProductCategoryUniqueSlug('products', $this->id)],
-            'images.files.*'       => ['nullable', 'mimes:bmp,jpeg,jpg,png,webp'],
+            'images.files.*'       => ['nullable', 'mimes:bmp,jpeg,jpg,png,webp,gif'],
             'images.positions.*'   => ['nullable', 'integer'],
             'videos.files.*'       => ['nullable', 'mimetypes:application/octet-stream,video/mp4,video/webm,video/quicktime', 'max:'.$this->maxVideoFileSize],
             'videos.positions.*'   => ['nullable', 'integer'],
@@ -93,7 +93,7 @@ class ProductForm extends FormRequest
             foreach (request()->images['files'] as $key => $file) {
                 if (Str::contains($key, 'image_')) {
                     $this->rules = array_merge($this->rules, [
-                        'images.files.'.$key => ['required', 'mimes:bmp,jpeg,jpg,png,webp'],
+                        'images.files.'.$key => ['required', 'mimes:bmp,jpeg,jpg,png,webp,gif'],
                     ]);
                 }
             }

+ 653 - 0
packages/Webkul/BagistoApi/docs/GRAPHQL_REVIEW_S3_UPLOAD.md

@@ -0,0 +1,653 @@
+# GraphQL Product Review 图片上传到 S3
+
+本文档说明如何通过 GraphQL mutation 创建产品评价并上传图片到 AWS S3。
+
+## 📋 目录
+
+- [GraphQL Mutation 定义](#graphql-mutation-定义)
+- [使用方法](#使用方法)
+- [图片格式说明](#图片格式说明)
+- [完整示例](#完整示例)
+- [前端集成](#前端集成)
+
+## GraphQL Mutation 定义
+
+### CreateProductReview Mutation
+
+```graphql
+mutation CreateProductReview($input: CreateProductReviewInput!) {
+  createProductReview(input: $input) {
+    id
+    productId
+    title
+    comment
+    rating
+    name
+    status
+    createdAt
+    attachments
+  }
+}
+```
+
+### Input 类型字段
+
+```graphql
+input CreateProductReviewInput {
+  productId: Int!          # 产品ID(必填)
+  title: String!           # 评价标题(必填)
+  comment: String!         # 评价内容(必填)
+  rating: Int!             # 评分 1-5(必填)
+  name: String!            # 评价者姓名(必填)
+  email: String            # 邮箱(可选)
+  status: Int              # 状态:0=pending, 1=approved(可选,默认pending)
+  
+  # 旧方式:Base64 JSON 字符串(向后兼容)
+  attachments: String      
+  
+  # 新方式:图片数组(推荐,支持S3上传)⭐
+  images: [ImageInput]     
+}
+
+input ImageInput {
+  data: String!            # Base64编码的图片数据
+  name: String             # 文件名(可选)
+  type: String             # MIME类型(可选)
+}
+```
+
+## 使用方法
+
+### 方法一:使用 images 数组(推荐)⭐
+
+这是新的推荐方式,会自动使用 ImageUploadService 上传到 S3。
+
+#### 简单 Base64 数组
+
+```graphql
+mutation {
+  createProductReview(input: {
+    productId: 1
+    title: "Great Product!"
+    comment: "I love this product. Highly recommended!"
+    rating: 5
+    name: "John Doe"
+    images: [
+      "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
+      "data:image/png;base64,iVBORw0KGgo..."
+    ]
+  }) {
+    id
+    title
+    attachments
+  }
+}
+```
+
+#### 结构化对象数组(更灵活)
+
+```graphql
+mutation {
+  createProductReview(input: {
+    productId: 1
+    title: "Amazing Quality"
+    comment: "The quality exceeded my expectations."
+    rating: 5
+    name: "Jane Smith"
+    images: [
+      {
+        data: "/9j/4AAQSkZJRg..."  # 不带 data URI 前缀的纯 Base64
+        name: "product_front.jpg"
+        type: "image/jpeg"
+      },
+      {
+        data: "iVBORw0KGgo..."
+        name: "product_side.png"
+        type: "image/png"
+      }
+    ]
+  }) {
+    id
+    title
+    attachments
+  }
+}
+```
+
+### 方法二:使用 attachments JSON 字符串(向后兼容)
+
+旧的方式仍然支持,用于向后兼容。
+
+```graphql
+mutation {
+  createProductReview(input: {
+    productId: 1
+    title: "Good Product"
+    comment: "Works as expected."
+    rating: 4
+    name: "Bob Wilson"
+    attachments: "[\"data:image/jpeg;base64,/9j/4AAQ...\"]"
+  }) {
+    id
+    title
+    attachments
+  }
+}
+```
+
+## 图片格式说明
+
+### 支持的图片格式
+
+- ✅ JPEG/JPG
+- ✅ PNG
+- ✅ GIF
+- ✅ WebP
+- ✅ SVG
+
+### 文件大小限制
+
+- 单张图片最大:**5 MB**
+- 建议数量:最多 **10 张**图片
+
+### Base64 格式
+
+#### 带 Data URI 前缀(标准格式)
+
+```
+data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...
+data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...
+```
+
+#### 纯 Base64(不带前缀)
+
+```
+/9j/4AAQSkZJRgABAQEAYABgAAD...
+```
+
+使用纯 Base64 时,需要在对象中指定 `type` 字段:
+
+```json
+{
+  "data": "/9j/4AAQSkZJRg...",
+  "name": "photo.jpg",
+  "type": "image/jpeg"
+}
+```
+
+## 完整示例
+
+### JavaScript/Fetch 示例
+
+```javascript
+// 将文件转换为 Base64
+function fileToBase64(file) {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.readAsDataURL(file);
+    reader.onload = () => resolve(reader.result);
+    reader.onerror = error => reject(error);
+  });
+}
+
+// 创建评价并上传图片
+async function createReviewWithImages(productId, reviewData, files) {
+  // 转换所有文件为 Base64
+  const images = await Promise.all(
+    files.map(async (file) => {
+      const base64 = await fileToBase64(file);
+      return {
+        data: base64,
+        name: file.name,
+        type: file.type
+      };
+    })
+  );
+
+  // GraphQL mutation
+  const mutation = `
+    mutation CreateProductReview($input: CreateProductReviewInput!) {
+      createProductReview(input: $input) {
+        id
+        productId
+        title
+        comment
+        rating
+        name
+        status
+        attachments
+        createdAt
+      }
+    }
+  `;
+
+  const variables = {
+    input: {
+      productId: productId,
+      title: reviewData.title,
+      comment: reviewData.comment,
+      rating: reviewData.rating,
+      name: reviewData.name,
+      email: reviewData.email,
+      images: images  // 使用新的 images 字段
+    }
+  };
+
+  try {
+    const response = await fetch('/graphql', {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': 'Bearer YOUR_TOKEN'  // 如果需要认证
+      },
+      body: JSON.stringify({
+        query: mutation,
+        variables: variables
+      })
+    });
+
+    const result = await response.json();
+    
+    if (result.errors) {
+      throw new Error(result.errors[0].message);
+    }
+
+    console.log('评价创建成功:', result.data.createProductReview);
+    return result.data.createProductReview;
+    
+  } catch (error) {
+    console.error('创建评价失败:', error);
+    throw error;
+  }
+}
+
+// 使用示例
+document.getElementById('reviewForm').addEventListener('submit', async (e) => {
+  e.preventDefault();
+  
+  const files = Array.from(document.getElementById('images').files);
+  
+  const reviewData = {
+    title: document.getElementById('title').value,
+    comment: document.getElementById('comment').value,
+    rating: parseInt(document.getElementById('rating').value),
+    name: document.getElementById('name').value,
+    email: document.getElementById('email').value
+  };
+  
+  try {
+    const result = await createReviewWithImages(1, reviewData, files);
+    alert('评价提交成功!');
+    console.log('附件信息:', result.attachments);
+  } catch (error) {
+    alert('提交失败: ' + error.message);
+  }
+});
+```
+
+### Vue.js 示例
+
+```vue
+<template>
+  <form @submit.prevent="submitReview">
+    <div>
+      <label>标题</label>
+      <input v-model="form.title" required />
+    </div>
+    
+    <div>
+      <label>评价内容</label>
+      <textarea v-model="form.comment" required></textarea>
+    </div>
+    
+    <div>
+      <label>评分</label>
+      <select v-model.number="form.rating">
+        <option v-for="n in 5" :key="n" :value="n">{{ n }} 星</option>
+      </select>
+    </div>
+    
+    <div>
+      <label>上传图片</label>
+      <input 
+        type="file" 
+        ref="fileInput"
+        multiple 
+        accept="image/*"
+        @change="handleFileSelect"
+      />
+    </div>
+    
+    <!-- 图片预览 -->
+    <div v-if="previews.length > 0" class="preview-grid">
+      <div v-for="(preview, index) in previews" :key="index">
+        <img :src="preview" style="max-width: 100px;" />
+      </div>
+    </div>
+    
+    <button type="submit" :disabled="uploading">
+      {{ uploading ? '上传中...' : '提交评价' }}
+    </button>
+  </form>
+</template>
+
+<script>
+export default {
+  props: {
+    productId: {
+      type: Number,
+      required: true
+    }
+  },
+  
+  data() {
+    return {
+      form: {
+        title: '',
+        comment: '',
+        rating: 5,
+        name: '',
+        email: ''
+      },
+      selectedFiles: [],
+      previews: [],
+      uploading: false
+    };
+  },
+  
+  methods: {
+    handleFileSelect(event) {
+      const files = Array.from(event.target.files);
+      this.selectedFiles = files;
+      
+      // 生成预览
+      this.previews = files.map(file => URL.createObjectURL(file));
+    },
+    
+    async fileToBase64(file) {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.readAsDataURL(file);
+        reader.onload = () => resolve(reader.result);
+        reader.onerror = reject;
+      });
+    },
+    
+    async submitReview() {
+      this.uploading = true;
+      
+      try {
+        // 转换文件为 Base64
+        const images = await Promise.all(
+          this.selectedFiles.map(async (file) => ({
+            data: await this.fileToBase64(file),
+            name: file.name,
+            type: file.type
+          }))
+        );
+        
+        // 调用 GraphQL
+        const response = await this.$apollo.mutate({
+          mutation: gql`
+            mutation CreateProductReview($input: CreateProductReviewInput!) {
+              createProductReview(input: $input) {
+                id
+                title
+                attachments
+              }
+            }
+          `,
+          variables: {
+            input: {
+              productId: this.productId,
+              ...this.form,
+              images: images
+            }
+          }
+        });
+        
+        this.$toast.success('评价提交成功!');
+        this.resetForm();
+        
+      } catch (error) {
+        this.$toast.error('提交失败: ' + error.message);
+      } finally {
+        this.uploading = false;
+      }
+    },
+    
+    resetForm() {
+      this.form = {
+        title: '',
+        comment: '',
+        rating: 5,
+        name: '',
+        email: ''
+      };
+      this.selectedFiles = [];
+      this.previews = [];
+      this.$refs.fileInput.value = '';
+    }
+  }
+};
+</script>
+```
+
+### React 示例
+
+```jsx
+import React, { useState } from 'react';
+import { useMutation, gql } from '@apollo/client';
+
+const CREATE_REVIEW_MUTATION = gql`
+  mutation CreateProductReview($input: CreateProductReviewInput!) {
+    createProductReview(input: $input) {
+      id
+      title
+      comment
+      rating
+      attachments
+    }
+  }
+`;
+
+function ReviewForm({ productId }) {
+  const [createReview] = useMutation(CREATE_REVIEW_MUTATION);
+  const [form, setForm] = useState({
+    title: '',
+    comment: '',
+    rating: 5,
+    name: '',
+    email: ''
+  });
+  const [files, setFiles] = useState([]);
+  const [uploading, setUploading] = useState(false);
+
+  const fileToBase64 = (file) => {
+    return new Promise((resolve, reject) => {
+      const reader = new FileReader();
+      reader.readAsDataURL(file);
+      reader.onload = () => resolve(reader.result);
+      reader.onerror = reject;
+    });
+  };
+
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    setUploading(true);
+
+    try {
+      // 转换文件
+      const images = await Promise.all(
+        files.map(async (file) => ({
+          data: await fileToBase64(file),
+          name: file.name,
+          type: file.type
+        }))
+      );
+
+      // 提交 mutation
+      const { data } = await createReview({
+        variables: {
+          input: {
+            productId,
+            ...form,
+            images
+          }
+        }
+      });
+
+      alert('评价提交成功!');
+      console.log('附件:', data.createProductReview.attachments);
+      
+      // 重置表单
+      setForm({ title: '', comment: '', rating: 5, name: '', email: '' });
+      setFiles([]);
+      
+    } catch (error) {
+      alert('提交失败: ' + error.message);
+    } finally {
+      setUploading(false);
+    }
+  };
+
+  return (
+    <form onSubmit={handleSubmit}>
+      <input
+        type="text"
+        placeholder="标题"
+        value={form.title}
+        onChange={(e) => setForm({...form, title: e.target.value})}
+        required
+      />
+      
+      <textarea
+        placeholder="评价内容"
+        value={form.comment}
+        onChange={(e) => setForm({...form, comment: e.target.value})}
+        required
+      />
+      
+      <select
+        value={form.rating}
+        onChange={(e) => setForm({...form, rating: parseInt(e.target.value)})}
+      >
+        {[1, 2, 3, 4, 5].map(n => (
+          <option key={n} value={n}>{n} 星</option>
+        ))}
+      </select>
+      
+      <input
+        type="file"
+        multiple
+        accept="image/*"
+        onChange={(e) => setFiles(Array.from(e.target.files))}
+      />
+      
+      <button type="submit" disabled={uploading}>
+        {uploading ? '上传中...' : '提交评价'}
+      </button>
+    </form>
+  );
+}
+
+export default ReviewForm;
+```
+
+## 响应格式
+
+成功的响应会包含上传后的图片信息:
+
+```json
+{
+  "data": {
+    "createProductReview": {
+      "id": 123,
+      "productId": 1,
+      "title": "Great Product!",
+      "comment": "I love this product!",
+      "rating": 5,
+      "name": "John Doe",
+      "status": 0,
+      "attachments": "[{\"type\":\"image\",\"url\":\"https://bucket.s3.region.amazonaws.com/review/123/abc123.webp\",\"path\":\"review/123/abc123.webp\"}]",
+      "createdAt": "2026-05-27 10:30:00"
+    }
+  }
+}
+```
+
+解析 `attachments` 字段:
+
+```javascript
+const attachments = JSON.parse(response.attachments);
+// [
+//   {
+//     "type": "image",
+//     "url": "https://bucket.s3.region.amazonaws.com/review/123/abc123.webp",
+//     "path": "review/123/abc123.webp"
+//   }
+// ]
+```
+
+## 错误处理
+
+### 常见错误
+
+1. **图片大小超限**
+   ```json
+   {
+     "errors": [{
+       "message": "File size exceeds the 5MB limit"
+     }]
+   }
+   ```
+
+2. **无效的图片格式**
+   ```json
+   {
+     "errors": [{
+       "message": "Invalid file format"
+     }]
+   }
+   ```
+
+3. **Base64 解码失败**
+   ```json
+   {
+     "errors": [{
+       "message": "Invalid base64 encoding"
+     }]
+   }
+   ```
+
+## 配置说明
+
+确保 `.env` 文件中正确配置了 S3:
+
+```env
+IMAGE_UPLOAD_DISK=s3
+
+AWS_ACCESS_KEY_ID=your-access-key
+AWS_SECRET_ACCESS_KEY=your-secret-key
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=your-bucket-name
+AWS_URL=https://your-bucket.s3.us-east-1.amazonaws.com
+```
+
+## 性能优化建议
+
+1. **前端压缩图片**:在上传前压缩图片,减少传输时间
+2. **限制图片数量**:建议最多上传 5-10 张
+3. **显示上传进度**:对于大文件,显示进度条
+4. **异步上传**:先提交评价文本,再异步上传图片
+
+## 安全注意事项
+
+1. ✅ 服务端验证文件类型和大小
+2. ✅ 使用 S3 预签名 URL(可选增强)
+3. ✅ 限制每个评价的图片数量
+4. ✅ 记录上传日志用于审计
+
+---
+
+**需要帮助?** 查看 [ImageUpload 模块文档](../ImageUpload/README.md)

+ 22 - 2
packages/Webkul/BagistoApi/src/Dto/CreateProductReviewInput.php

@@ -33,8 +33,26 @@ class CreateProductReviewInput
     #[Groups(['mutation'])]
     public ?int $status = null;
 
+    /**
+     * 支持两种格式:
+     * 1. Base64 编码的图片数据(向后兼容)
+     * 2. JSON 数组,包含图片信息:[{"file": "base64_data", "name": "filename.jpg"}]
+     * 
+     * @var string|null JSON 字符串或 Base64 数据
+     */
     #[Groups(['mutation'])]
-    public ?string $attachments;
+    public ?string $attachments = null;
+
+    /**
+     * 新字段:直接传递图片文件数组(推荐方式)
+     * 每个元素可以是:
+     * - Base64 编码字符串
+     * - 或者包含 {data, name, type} 的对象
+     * 
+     * @var array|null
+     */
+    #[Groups(['mutation'])]
+    public ?array $images = null;
 
     public function __construct(
         int $productId,
@@ -44,7 +62,8 @@ class CreateProductReviewInput
         string $name,
         ?string $email = null,
         ?int $status = null,
-        ?string $attachments = '',
+        ?string $attachments = null,
+        ?array $images = null,
     ) {
         $this->productId = $productId;
         $this->title = $title;
@@ -54,5 +73,6 @@ class CreateProductReviewInput
         $this->email = $email;
         $this->status = $status;
         $this->attachments = $attachments;
+        $this->images = $images;
     }
 }

+ 5 - 3
packages/Webkul/BagistoApi/src/Providers/BagistoApiServiceProvider.php

@@ -91,6 +91,7 @@ use Webkul\BagistoApi\State\DeleteAllWishlistsProcessor;
 use Webkul\BagistoApi\State\CancelOrderProcessor;
 use Webkul\BagistoApi\State\ReorderProcessor;
 use Webkul\BagistoApi\State\DeleteAllCompareItemsProcessor;
+use Longyi\ImageUpload\Services\ImageUploadService;
 use ApiPlatform\GraphQl\Serializer\SerializerContextBuilder as GraphQlSerializerContextBuilder;
 use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
 use Webkul\BagistoApi\GraphQl\Serializer\FixedSerializerContextBuilder;
@@ -207,7 +208,8 @@ class BagistoApiServiceProvider extends ServiceProvider
 
         $this->app->singleton(ProductReviewProcessor::class, function ($app) {
             return new ProductReviewProcessor(
-                $app->make(PersistProcessor::class)
+                $app->make(PersistProcessor::class),
+                $app->make(ImageUploadService::class)
             );
         });
 
@@ -359,7 +361,7 @@ class BagistoApiServiceProvider extends ServiceProvider
                 $app->make(Pagination::class)
             );
         });
-        
+
         $this->app->singleton(ProductRelationProvider::class, function ($app) {
             return new ProductRelationProvider(
                 $app->make(Pagination::class)
@@ -545,7 +547,7 @@ class BagistoApiServiceProvider extends ServiceProvider
         } else {
             $this->publishes([
                 __DIR__.'/../config/api-platform.php' => config_path('api-platform.php'),
-            ], 'bagistoapi-config');        
+            ], 'bagistoapi-config');
         }
 
         $this->publishes([

+ 186 - 6
packages/Webkul/BagistoApi/src/State/ProductReviewProcessor.php

@@ -6,6 +6,7 @@ use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProcessorInterface;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Storage;
+use Longyi\ImageUpload\Services\ImageUploadService;
 use Webkul\BagistoApi\Dto\CreateProductReviewInput;
 use Webkul\BagistoApi\Dto\UpdateProductReviewInput;
 use Webkul\BagistoApi\Exception\AuthorizationException;
@@ -19,11 +20,13 @@ use Webkul\BagistoApi\Models\ProductReviewAttachment;
 /**
  * ProductReviewProcessor - Handles create/update operations for product reviews
  * Validates input and delegates persistence to the persist processor
+ * 支持通过 ImageUploadService 上传图片到 S3
  */
 class ProductReviewProcessor implements ProcessorInterface
 {
     public function __construct(
-        private readonly ProcessorInterface $persistProcessor
+        private readonly ProcessorInterface $persistProcessor,
+        private readonly ImageUploadService $imageUploadService
     ) {}
 
     public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
@@ -88,9 +91,16 @@ class ProductReviewProcessor implements ProcessorInterface
 
         $review->save();
 
+        // 触发评价创建事件(用于积分系统等)
+        event('customer.review.create.after', $review);
+
+        // 处理图片上传(新方式:使用 images 字段)
         $attachments = [];
 
-        if (! empty($data->attachments) && $review?->id) {
+        if (! empty($data->images) && is_array($data->images)) {
+            $attachments = $this->saveImagesToS3($data->images, $review->id);
+        } elseif (! empty($data->attachments)) {
+            // 向后兼容:使用旧的 attachments 字段(Base64格式)
             $attachments = $this->saveAttachments($data->attachments, $review->id);
         }
 
@@ -138,14 +148,19 @@ class ProductReviewProcessor implements ProcessorInterface
 
         $review->save();
 
+        // 处理图片上传(新方式:使用 images 字段)
         $attachments = [];
 
-        if (! empty($data->attachments) && $review?->id) {
+        if (! empty($data->images) && is_array($data->images)) {
+            $attachments = $this->saveImagesToS3($data->images, $review->id);
+        } elseif (! empty($data->attachments)) {
+            // 向后兼容:使用旧的 attachments 字段(Base64格式)
             $attachments = $this->saveAttachments($data->attachments, $review->id);
         }
 
         if ($attachments) {
             $review->setAttribute('attachments', json_encode($attachments));
+            $review->save();
         }
 
 	    return $this->mapToOutput($review);
@@ -206,7 +221,7 @@ class ProductReviewProcessor implements ProcessorInterface
                 case 'product_id':
                 case 'rating':
                     $output->$key = (int)$value;
-                    
+
                     break;
 
                 case 'title':
@@ -224,7 +239,7 @@ class ProductReviewProcessor implements ProcessorInterface
                     if ($value instanceof \DateTime) {
                         $output->$key = $value->format('Y-m-d H:i:s');
                     } else {
-                        $output->$key = $value; 
+                        $output->$key = $value;
                     }
                     break;
                 default:
@@ -232,7 +247,7 @@ class ProductReviewProcessor implements ProcessorInterface
                     break;
             }
         }
- 
+
         return $output;
     }
 
@@ -249,6 +264,171 @@ class ProductReviewProcessor implements ProcessorInterface
         throw new InvalidInputException(__('Invalid file format'));
     }
 
+    /**
+     * 使用 ImageUploadService 上传图片到 S3
+     * 支持多种输入格式:
+     * 1. Base64 字符串数组
+     * 2. 对象数组:[{data: "base64...", name: "file.jpg", type: "image/jpeg"}]
+     *
+     * @param array $images 图片数据数组
+     * @param int $reviewId 评价ID
+     * @return array 上传结果数组
+     */
+    private function saveImagesToS3(array $images, int $reviewId): array
+    {
+        $attachmentUrls = [];
+        $directory = "uploads";
+
+        foreach ($images as $index => $imageData) {
+            try {
+                // 处理不同格式的输入
+                if (is_string($imageData)) {
+                    // Base64 字符串
+                    $result = $this->uploadBase64ImageToS3($imageData, $directory, $reviewId);
+                } elseif (is_array($imageData) || is_object($imageData)) {
+                    // 对象格式:{data, name, type}
+                    $data = is_object($imageData) ? (array) $imageData : $imageData;
+                    $result = $this->uploadStructuredImageToS3($data, $directory, $reviewId);
+                } else {
+                    continue;
+                }
+
+                if ($result) {
+                    $attachmentUrls[] = $result;
+                }
+            } catch (\Exception $e) {
+                report($e);
+                // 继续处理其他图片,不中断整个流程
+            }
+        }
+
+        return $attachmentUrls;
+    }
+
+    /**
+     * 上传 Base64 编码的图片到 S3
+     */
+    private function uploadBase64ImageToS3(string $base64Data, string $directory, int $reviewId): ?array
+    {
+        // 解析 Base64 数据
+        if (! preg_match('/^data:(image|video)\/(\w+);base64,/', $base64Data, $matches)) {
+            // 尝试直接解码(不带 data URI 前缀)
+            $decoded = base64_decode($base64Data, true);
+            if ($decoded === false) {
+                throw new InvalidInputException(__('bagistoapi::app.graphql.upload.invalid-base64'));
+            }
+
+            // 默认为 JPEG
+            $extension = 'jpg';
+            $mimeType = 'image/jpeg';
+            $mediaType = 'image';
+        } else {
+            $mediaType = $matches[1];
+            $extension = $matches[2];
+            $mimeType = "{$mediaType}/{$extension}";
+
+            $pure = substr($base64Data, strpos($base64Data, ',') + 1);
+            $decoded = base64_decode($pure, true);
+
+            if ($decoded === false) {
+                throw new InvalidInputException(__('bagistoapi::app.graphql.upload.invalid-base64'));
+            }
+        }
+
+        // 检查文件大小(5MB限制)
+        if (strlen($decoded) > 5 * 1024 * 1024) {
+            throw new InvalidInputException(__('bagistoapi::app.graphql.upload.size-exceeds-limit'));
+        }
+
+        // 创建临时文件
+        $tempFile = tmpfile();
+        $tempPath = stream_get_meta_data($tempFile)['uri'];
+        fwrite($tempFile, $decoded);
+        rewind($tempFile);
+
+        try {
+            // 使用 ImageUploadService 上传到 S3
+            $uploadedFile = new \Illuminate\Http\UploadedFile(
+                $tempPath,
+                "review_image.{$extension}",
+                $mimeType,
+                null,
+                true
+            );
+
+            $result = $this->imageUploadService->upload($uploadedFile, $directory);
+
+            if ($result['success']) {
+                // 保存到数据库
+                ProductReviewAttachment::create([
+                    'review_id' => $reviewId,
+                    'path'      => $result['path'],
+                    'type'      => $mediaType,
+                    'mime_type' => $mimeType,
+                ]);
+
+                return [
+                    'type' => $mediaType,
+                    'url'  => $result['url'],
+                    'path' => $result['path'],
+                ];
+            }
+
+            return null;
+        } finally {
+            fclose($tempFile);
+        }
+    }
+
+    /**
+     * 上传结构化数据格式的图片到 S3
+     * 期望格式:{data: "base64...", name: "file.jpg", type: "image/jpeg"}
+     */
+    private function uploadStructuredImageToS3(array $data, string $directory, int $reviewId): ?array
+    {
+        $base64Data = $data['data'] ?? $data['file'] ?? null;
+        $fileName = $data['name'] ?? 'review_image.jpg';
+        $mimeType = $data['type'] ?? $data['mime_type'] ?? null;
+
+        if (! $base64Data) {
+            throw new InvalidInputException('Image data is required');
+        }
+
+        // 如果有 data URI 前缀,直接使用 uploadBase64ImageToS3
+        if (str_starts_with($base64Data, 'data:')) {
+            return $this->uploadBase64ImageToS3($base64Data, $directory, $reviewId);
+        }
+
+        // 否则需要 mimeType
+        if (! $mimeType) {
+            // 根据文件扩展名推断
+            $extension = pathinfo($fileName, PATHINFO_EXTENSION);
+            $mimeType = $this->getMimeTypeFromExtension($extension);
+        }
+
+        // 构建 data URI
+        $dataUri = "data:{$mimeType};base64,{$base64Data}";
+
+        return $this->uploadBase64ImageToS3($dataUri, $directory, $reviewId);
+    }
+
+    /**
+     * 根据文件扩展名获取 MIME 类型
+     */
+    private function getMimeTypeFromExtension(string $extension): string
+    {
+        $mimeTypes = [
+            'jpg' => 'image/jpeg',
+            'jpeg' => 'image/jpeg',
+            'png' => 'image/png',
+            'gif' => 'image/gif',
+            'webp' => 'image/webp',
+            'svg' => 'image/svg+xml',
+        ];
+
+        return $mimeTypes[strtolower($extension)] ?? 'image/jpeg';
+    }
+
     /**
      * Handle image upload with base64 encoding
      */

+ 40 - 8
packages/Webkul/Product/src/Repositories/ProductMediaRepository.php

@@ -2,6 +2,7 @@
 
 namespace Webkul\Product\Repositories;
 
+use Aws\S3\S3Client;
 use Exception;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Support\Facades\Storage;
@@ -33,7 +34,7 @@ class ProductMediaRepository extends Repository
      */
     public function getProductDirectory($product): string
     {
-        return 'product/'.$product->id;
+        return 'uploads/'.date('Ym/d');
     }
 
     /**
@@ -55,13 +56,44 @@ class ProductMediaRepository extends Repository
             foreach ($data[$uploadFileType]['files'] as $indexOrModelId => $file) {
                 if ($file instanceof UploadedFile) {
                     if (Str::contains($file->getMimeType(), 'image')) {
-                        $manager = new ImageManager;
-
-                        $image = $manager->make($file)->encode('webp');
-
-                        $path = $this->getProductDirectory($product).'/'.Str::random(40).'.webp';
-
-                        Storage::put($path, $image);
+                        $extension = strtolower($file->getClientOriginalExtension());
+
+                        // 使用原生 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 验证用于测试
+                            ],
+                        ]);
+
+                        if ($extension === 'gif') {
+                            // GIF 动画:直接上传原始文件,不经 ImageManager 处理以保留动画帧
+                            $path = $this->getProductDirectory($product) . '/' . Str::random(40) . '.gif';
+
+                            $result = $s3Client->putObject([
+                                'Bucket'      => env('AWS_BUCKET'),
+                                'Key'         => $path,
+                                'Body'        => fopen($file->getRealPath(), 'r'),
+                                'ContentType' => 'image/gif',
+                            ]);
+                        } else {
+                            // 其他图片:转为 webp 后上传
+                            $manager = new ImageManager;
+                            $image = $manager->make($file)->encode('webp');
+                            $path = $this->getProductDirectory($product) . '/' . Str::random(40) . '.webp';
+
+                            $result = $s3Client->putObject([
+                                'Bucket'      => env('AWS_BUCKET'),
+                                'Key'         => $path,
+                                'Body'        => (string) $image,
+                                'ContentType' => 'image/webp',
+                            ]);
+                        }
                     } else {
                         $path = $file->store($this->getProductDirectory($product));
                     }