Преглед изворни кода

Merge branch 'dev' of http://gogs.hnwmzp.cn/chengwenliang/nshop into dev

chengwl пре 13 часа
родитељ
комит
60fcf11e41

+ 0 - 0
bootstrap/cache/packages.php


+ 0 - 0
bootstrap/cache/services.php


+ 21 - 3
config/elasticsearch.php

@@ -54,12 +54,30 @@ return [
     /**
      * CA Bundle
      *
-     * If you have the http_ca.crt certificate copied during the start of Elasticsearch
-     * then the path here
+     * OrbStack (*.orb.local) uses its own root CA; direct ES uses http_ca.crt.
+     * Override with ELASTICSEARCH_CA_BUNDLE in .env when needed.
      *
      * @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/connecting.html#auth-http
      */
-    'caBundle' => null,
+    'caBundle' => (function () {
+        $bundle = env('ELASTICSEARCH_CA_BUNDLE');
+
+        if ($bundle && ! str_starts_with($bundle, '/')) {
+            $bundle = base_path($bundle);
+        }
+
+        if ($bundle && is_readable($bundle)) {
+            return $bundle;
+        }
+
+        foreach ([public_path('orbstack_ca.crt'), public_path('elastic_ca.crt')] as $candidate) {
+            if (is_readable($candidate)) {
+                return $candidate;
+            }
+        }
+
+        return null;
+    })(),
 
     /**
      * Retries

+ 58 - 1
packages/Longyi/Core/src/Helpers/ProductImage.php

@@ -2,6 +2,7 @@
 
 namespace Longyi\Core\Helpers;
 
+use Illuminate\Support\Str;
 use Webkul\Product\ProductImage as BaseProductImage;
 
 /**
@@ -12,10 +13,57 @@ use Webkul\Product\ProductImage as BaseProductImage;
  *   - getSmallImage($product)     → image with is_small_image = true (falls back to first)
  *   - getThumbnailImage($product) → image with is_thumbnail = true  (falls back to first)
  *
- * Also overrides getProductBaseImage() to honour the is_base_image flag.
+ * Also overrides getProductBaseImage() to honour the is_base_image flag, and
+ * supports remote/absolute image URLs stored directly in product_images.path.
  */
 class ProductImage extends BaseProductImage
 {
+    /**
+     * Resolve gallery images, honouring remote/absolute URLs stored in the
+     * `path` column (which Storage::has() / the local cache resizer can't serve).
+     *
+     * @param  \Webkul\Product\Contracts\Product  $product
+     * @return array
+     */
+    public function getGalleryImages($product)
+    {
+        if (! $product) {
+            return [];
+        }
+
+        $images = [];
+
+        foreach ($product->images as $image) {
+            if (! $this->isAbsoluteUrl($image->path) && ! \Illuminate\Support\Facades\Storage::has($image->path)) {
+                continue;
+            }
+
+            $images[] = $this->getCachedImageUrlsPublic($image->path);
+        }
+
+        if (
+            ! $product->parent_id
+            && ! count($images)
+            && ! count($product->videos ?? [])
+        ) {
+            $images[] = $this->getFallbackImageUrlsPublic();
+        }
+
+        if (empty($images) && $product->parent) {
+            $images = $this->getGalleryImages($product->parent);
+        }
+
+        return $images;
+    }
+
+    /**
+     * Determine whether a path is an absolute (remote) URL.
+     */
+    protected function isAbsoluteUrl(?string $path): bool
+    {
+        return is_string($path) && Str::startsWith($path, ['http://', 'https://']);
+    }
+
     /**
      * Get the image designated as the base image.
      * Falls back to the first image if none is explicitly set.
@@ -116,6 +164,15 @@ class ProductImage extends BaseProductImage
      */
     protected function getCachedImageUrlsPublic(string $path): array
     {
+        if ($this->isAbsoluteUrl($path)) {
+            return [
+                'small_image_url'    => $path,
+                'medium_image_url'   => $path,
+                'large_image_url'    => $path,
+                'original_image_url' => $path,
+            ];
+        }
+
         if (! $this->isDriverLocalPublic()) {
             return [
                 'small_image_url'    => \Illuminate\Support\Facades\Storage::url($path),

+ 19 - 0
packages/Longyi/Core/src/Models/ProductImage.php

@@ -3,6 +3,7 @@
 namespace Longyi\Core\Models;
 
 use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
 use Webkul\Product\Models\ProductImage as BaseProductImage;
 use Webkul\Product\Models\ProductProxy;
 
@@ -78,6 +79,24 @@ class ProductImage extends BaseProductImage
         return in_array($role, $this->getRolesAttribute(), true);
     }
 
+    /**
+     * Get image url for the product image.
+     *
+     * Supports remote/absolute URLs stored directly in the `path` column
+     * (e.g. imported product images) by returning them verbatim instead of
+     * prefixing the local storage path.
+     *
+     * @return string
+     */
+    public function url()
+    {
+        if (is_string($this->path) && Str::startsWith($this->path, ['http://', 'https://'])) {
+            return $this->path;
+        }
+
+        return Storage::url($this->path);
+    }
+
     /**
      * Scope: filter images that are designated as base image.
      */

+ 1 - 0
packages/Longyi/Core/src/Models/ProductVariant.php

@@ -246,3 +246,4 @@ class ProductVariant extends Model implements ProductVariantContract, HasMedia
         return $query->where('status', true)->where('quantity', '>', 0);
     }
 }
+

+ 1 - 1
packages/Webkul/Admin/src/Http/Controllers/Catalog/ProductController.php

@@ -92,7 +92,7 @@ class ProductController extends Controller
         $validationRules = [
             'type' => 'required',
             'attribute_family_id' => 'required',
-            'sku' => ['required', 'unique:products,sku', new Slug],
+            'sku' => ['required', 'unique:products,sku'],
         ];
         if ($productType !== 'flexible_variant') {
             $validationRules['super_attributes'] = 'array|min:1';

+ 5 - 2
packages/Webkul/Admin/src/Resources/views/catalog/products/edit.blade.php

@@ -161,9 +161,12 @@
                         @php $customAttributes = $product->getEditableAttributes($group); @endphp
 
                         @if (
-                            $group->code === 'inventories' 
+                            $group->code === 'inventories'
                             && (
-                                $product->getTypeInstance()->isComposite()
+                                (
+                                    $product->getTypeInstance()->isComposite()
+                                    && $product->type !== 'flexible_variant'
+                                )
                                 || $product->type === 'downloadable'
                             )
                         )

+ 0 - 1
packages/Webkul/Admin/src/Resources/views/catalog/products/edit/inventories.blade.php

@@ -1,5 +1,4 @@
 {!! view_render_event('bagisto.admin.catalog.product.edit.form.inventories.controls.before', ['product' => $product]) !!}
-
 <v-inventories>
     <!-- Panel Content -->
     <div class="mb-5 text-sm text-gray-600 dark:text-gray-300">

+ 2 - 2
packages/Webkul/BagistoApi/src/GraphQl/Serializer/FixedSerializerContextBuilder.php

@@ -148,7 +148,7 @@ class FixedSerializerContextBuilder implements SerializerContextBuilderInterface
      */
     private function addQtyFieldsToAttributes(array &$attributes): void
     {
-        $qtyFields = ['qty_ordered', 'qty_shipped', 'qty_invoiced', 'qty_canceled', 'qty_refunded'];
+        $qtyFields = ['qty_ordered', 'qty_shipped', 'qty_invoiced', 'qty_canceled', 'qty_refunded', 'baseImage'];
         
         foreach ($qtyFields as $field) {
             $attributes[$field] = true;
@@ -234,7 +234,7 @@ class FixedSerializerContextBuilder implements SerializerContextBuilderInterface
         $attributes = $context['attributes'] ?? [];
         
         // Use snake_case field names to match the denormalization used by the serializer
-        $qtyFields = ['id', 'qty_ordered', 'qty_shipped', 'qty_invoiced', 'qty_canceled', 'qty_refunded', 'sku', 'name', 'price', 'base_price', 'total', 'base_total'];
+        $qtyFields = ['id', 'qty_ordered', 'qty_shipped', 'qty_invoiced', 'qty_canceled', 'qty_refunded', 'sku', 'name', 'price', 'base_price', 'total', 'base_total', 'type', 'baseImage'];
         
         // Ensure attributes includes all qty fields
         foreach ($qtyFields as $field) {

+ 81 - 0
packages/Webkul/BagistoApi/tests/Feature/Rest/CustomerOrderRestTest.php

@@ -3,8 +3,10 @@
 namespace Webkul\BagistoApi\Tests\Feature\Rest;
 
 use Webkul\BagistoApi\Tests\RestApiTestCase;
+use Webkul\Checkout\Models\Cart;
 use Webkul\Core\Models\Channel;
 use Webkul\Product\Models\Product;
+use Webkul\BagistoApi\Models\GuestCartTokens;
 use Webkul\Sales\Models\Order;
 use Webkul\Sales\Models\OrderItem;
 use Webkul\Sales\Models\OrderPayment;
@@ -67,6 +69,51 @@ class CustomerOrderRestTest extends RestApiTestCase
         return compact('customer', 'channel', 'product', 'order1', 'order2');
     }
 
+    /**
+     * Create guest order with payment additional cart_token/cart_id.
+     */
+    private function createGuestOrderData(): array
+    {
+        $this->seedRequiredData();
+
+        $channel = Channel::first();
+        $product = Product::factory()->create();
+        $cart = Cart::factory()->create(['customer_id' => null]);
+        $guestToken = 'guest-token-'.uniqid();
+
+        GuestCartTokens::query()->create([
+            'token'   => $guestToken,
+            'cart_id' => $cart->id,
+        ]);
+
+        $order = Order::factory()->create([
+            'customer_id'   => null,
+            'customer_type' => null,
+            'is_guest'      => 1,
+            'customer_email'=> 'guest@example.com',
+            'channel_id'    => $channel->id,
+            'status'        => 'pending',
+        ]);
+
+        OrderItem::factory()->create([
+            'order_id'   => $order->id,
+            'product_id' => $product->id,
+            'sku'        => 'GUEST-TEST-SKU-001',
+            'type'       => 'simple',
+            'name'       => 'Guest Test Product',
+        ]);
+
+        OrderPayment::factory()->create([
+            'order_id'    => $order->id,
+            'additional'  => [
+                'cart_token' => $guestToken,
+                'cart_id'    => $cart->id,
+            ],
+        ]);
+
+        return compact('order', 'guestToken');
+    }
+
     // ── Collection ────────────────────────────────────────────
 
     /**
@@ -224,6 +271,40 @@ class CustomerOrderRestTest extends RestApiTestCase
         expect(in_array($response->getStatusCode(), [401, 403, 500]))->toBeTrue();
     }
 
+    /**
+     * Test: Guest can access own order detail with guest token.
+     */
+    public function test_guest_can_get_own_order_detail(): void
+    {
+        $guestData = $this->createGuestOrderData();
+
+        $response = $this->guestGet(
+            $guestData['guestToken'],
+            '/api/shop/customer-orders/'.$guestData['order']->id
+        );
+
+        $response->assertOk();
+        $json = $response->json();
+
+        expect($json['id'])->toBe($guestData['order']->id);
+        expect($json['isGuest'])->toBeTrue();
+    }
+
+    /**
+     * Test: Guest cannot access others order detail.
+     */
+    public function test_guest_cannot_get_other_order_detail(): void
+    {
+        $guestData = $this->createGuestOrderData();
+
+        $response = $this->guestGet(
+            'guest-token-wrong',
+            '/api/shop/customer-orders/'.$guestData['order']->id
+        );
+
+        expect(in_array($response->getStatusCode(), [401, 403, 404, 500]))->toBeTrue();
+    }
+
     // ── Response Shape ────────────────────────────────────────
 
     /**

+ 11 - 0
packages/Webkul/BagistoApi/tests/RestApiTestCase.php

@@ -66,4 +66,15 @@ abstract class RestApiTestCase extends BagistoApiTestCase
     {
         return $this->deleteJson($url, [], $this->storefrontHeaders());
     }
+
+    /**
+     * Execute a guest GET request (storefront key + guest bearer token).
+     */
+    protected function guestGet(string $guestToken, string $url): TestResponse
+    {
+        return $this->withHeaders([
+            ...$this->storefrontHeaders(),
+            'Authorization' => "Bearer {$guestToken}",
+        ])->getJson($url);
+    }
 }

+ 2 - 2
packages/Webkul/Core/src/Rules/Slug.php

@@ -12,8 +12,8 @@ class Slug implements ValidationRule
      */
     public function validate(string $attribute, mixed $value, Closure $fail): void
     {
-        if (! preg_match('/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/', $value)) {
-            $fail('core::validation.slug')->translate();
+        if (! preg_match('/^[\p{L}\p{M}\p{N}]+(?:-[\p{L}\p{M}\p{N}]+)*$/u', $value)) {
+            $fail(__('core::validation.slug'));
         }
     }
 }

+ 2 - 2
packages/Webkul/Paypal/src/Http/routes.php

@@ -16,7 +16,7 @@ Route::group(['middleware' => ['web']], function () {
     Route::prefix('paypal/smart-button')->group(function () {
         Route::get('/create-order', [SmartButtonController::class, 'createOrder'])->name('paypal.smart-button.create-order');
 
-        Route::post('/capture-order', [SmartButtonController::class, 'captureOrder'])->name('paypal.smart-button.capture-order');
+        Route::post('/capture-order', [SmartButtonController::class, 'captureOrder'])->name('paypal.smart-button.capture-order')->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class);
     });
 });
 
@@ -32,7 +32,7 @@ if (class_exists(\Webkul\BagistoApi\Http\Middleware\VerifyStorefrontKey::class))
         Route::prefix('paypal/smart-button')->group(function () {
             Route::get('/create-order', [SmartButtonController::class, 'createOrder'])->name('bagistoapi.paypal.smart-button.create-order');
 
-            Route::post('/capture-order', [SmartButtonController::class, 'captureOrder'])->name('bagistoapi.paypal.smart-button.capture-order');
+            Route::post('/capture-order', [SmartButtonController::class, 'captureOrder'])->name('bagistoapi.paypal.smart-button.capture-order')->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class);
         });
     });
 }

+ 8 - 0
packages/Webkul/Product/src/Models/ProductImage.php

@@ -4,6 +4,7 @@ namespace Webkul\Product\Models;
 
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
 use Webkul\Product\Contracts\ProductImage as ProductImageContract;
 
 class ProductImage extends Model implements ProductImageContract
@@ -51,6 +52,13 @@ class ProductImage extends Model implements ProductImageContract
      */
     public function url()
     {
+        // Imported product images may store a remote/absolute URL directly in
+        // the `path` column; return it verbatim instead of prefixing the local
+        // storage path.
+        if (is_string($this->path) && Str::startsWith($this->path, ['http://', 'https://'])) {
+            return $this->path;
+        }
+
         return Storage::url($this->path);
     }
 

+ 0 - 1
packages/Webkul/Product/src/Type/Simple.php

@@ -153,7 +153,6 @@ class Simple extends AbstractType
         if (! $this->product->manage_stock) {
             return true;
         }
-
         return $qty <= $this->totalQuantity() ?: (bool) core()->getConfigData('catalog.inventory.stock_options.back_orders');
     }
 

+ 5 - 0
phpunit.xml

@@ -30,6 +30,11 @@
         <testsuite name="Shop Feature Test">
             <directory suffix="Test.php">packages/Webkul/Shop/tests/Feature</directory>
         </testsuite>
+
+        <!-- BagistoApi package testsuites. -->
+        <testsuite name="BagistoApi Unit Test">
+            <directory suffix="Test.php">packages/Webkul/BagistoApi/tests/Unit</directory>
+        </testsuite>
     </testsuites>
 
     <source>